Java NIO服務端程式碼的hello world如下
public class NBTimeServer {
public static void main(String[] args) {
try {
Selector acceptSelector = SelectorProvider.provider().openSelector();
//建立一個新的server socket,設定為非阻塞模式
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
// 繫結server sokcet到本機和對應的埠
InetAddress lh = InetAddress.getLocalHost();
InetSocketAddress isa = new InetSocketAddress(lh, 8900);
ssc.socket().bind(isa);
//通過selector註冊server socket,這裡即告訴selector,當accept發生的時候,socket會被放在reday佇列
SelectionKey acceptKey = ssc.register(acceptSelector,
SelectionKey.OP_ACCEPT);
int keysAdded = 0;
// 當任何一個註冊事件發生的時候,select就會返回
while ((keysAdded = acceptSelector.select()) > 0) {
// 獲取已經準備好的selectorkey
Set readyKeys = acceptSelector.selectedKeys();
Iterator i = readyKeys.iterator();
while (i.hasNext()) {
SelectionKey sk = (SelectionKey)i.next();
i.remove();
ServerSocketChannel nextReady =
(ServerSocketChannel)sk.channel();
Socket s = nextReady.accept().socket();
PrintWriter out = new PrintWriter(s.getOutputStream(), true);
Date now = new Date();
out.println(now);
out.close();
}
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
複製程式碼
1: 獲取selector。
SelectorProvider提供的所有provider都是同一個物件。如果沒有,它會通過AccessController.doPrivileged
來給獲取provider的程式碼最高的許可權,執行邏輯是:
- java.nio.channels.spi.SelectorProvider 是否有配置,有就通過反射建立(本例沒有)
- 是不是在jar中已經例項化了 java.nio.channels.spi.SelectorProvider,並且他能夠通過getSystemClassLoader載入,就是用第一個獲取到的SelectorProvider(本例沒有)
- 最終通過sun.nio.ch.DefaultSelectorProvider類來建立,它在不同的作業系統下有著不同的實現
if ("SunOS".equals(osname)) {
return new sun.nio.ch.DevPollSelectorProvider();
}
if("Linux".equals(osname)){
if (major > 2 || (major == 2 && minor >= 6)) {
return new sun.nio.ch.EPollSelectorProvider();
}
}
return new sun.nio.ch.PollSelectorProvider(); //預設返回
複製程式碼
程式碼存在縮減,只取核心
類之間的關係如下
下面只關注Epoll和Poll
拿到provider之後,開始執行openSelector,獲取真正的selector。
對於poll,返回的例項是PollSelectorImpl,對於Epoll返回的例項則是EpollSelectorImpl。
file descriptor :unix設計哲學就是一切都是檔案,它可能是一個網路連線、一個終端等等。它本身就是一個數值,在系統中會維護檔案描述符和它對應檔案的一個指標,從而找到對應的檔案操作
- fd0的獲取主要是呼叫Native方法實現
long pipeFds = IOUtil.makePipe(false);
fd0 = (int) (pipeFds >>> 32); // >>> 表示無符號右移,最高位補0,這裡即獲取讀檔案描述符
fd1 = (int) pipeFds; //截掉了高位,儲存的是寫檔案描述符
複製程式碼
IOUtil針對不同的作業系統有不同的實現,以solaris為例,它的實現在IOUtil.c中,主要實現即通過Linux pipe方法和Linux fcntl方法 (程式碼有刪減)
int fd[2]; if (pipe(fd) < 0) // 獲取讀和寫的檔案符 if ((configureBlocking(fd[0], JNI_FALSE) < 0) //標註為非阻塞 || (configureBlocking(fd[1], JNI_FALSE) < 0)) return ((jlong) fd[0] << 32) | (jlong) fd[1]; //讀的檔案描述符放在高位,寫的檔案描述符放在低位 複製程式碼
configureBlocking本身的實現在IOUtil.c中
static int configureBlocking(int fd, jboolean blocking) //設定為非阻塞狀態 { int flags = fcntl(fd, F_GETFL); int newflags = blocking ? (flags & ~O_NONBLOCK) : (flags | O_NONBLOCK); return (flags == newflags) ? 0 : fcntl(fd, F_SETFL, newflags); } 複製程式碼
pipe實際是建立了一個程式間通訊的單向資料管道,引數中的fd[0]表示管道讀取端的結尾,fd[1]表示管道寫端的結尾;fcntl則主要是根據第二個引數,如原始碼中的F_GETFL和F_SETFL,對第一個引數執行對應的操作;
- 新建EPollArrayWrapper,部分欄位如下
pollWrapper = new EPollArrayWrapper();
pollWrapper.initInterrupt(fd0, fd1);
複製程式碼
- epfd:通過Native方法去構建,對應的實現在EPollArrayWrapper.c中,方法為:Java_sun_nio_ch_EPollArrayWrapper_epollCreate,主要的實現邏輯是
int epfd = (*epoll_create_func)(256);
而epoll_create_func在Java_sun_nio_ch_EPollArrayWrapper_init執行的時候已經是執行了初始化,對應的是Linux epoll_create ,返回既是一個epoll例項,它實質也是一個檔案描述符
epoll_create_func = (epoll_create_t) dlsym(RTLD_DEFAULT, "epoll_create"); epoll_ctl_func = (epoll_ctl_t) dlsym(RTLD_DEFAULT, "epoll_ctl"); epoll_wait_func = (epoll_wait_t) dlsym(RTLD_DEFAULT, "epoll_wait"); 複製程式碼
- pollArray:一個用來儲存從epoll_wait中得到結果的陣列,它的大小為
NUM_EPOLLEVENTS * SIZE_EPOLLEVENT
,其中的NUM_EPOLLEVENTS則是取的檔案描述符限制和8192相比的最小值Math.min(fdLimit(), 8192);
詳見Linux getrlimit,實質是AllocatedNativeObject
- initInterrupt:出了儲存對應的檔案描述符之外,還執行了
epollCtl(epfd, EPOLL_CTL_ADD, fd0, EPOLLIN);
,即把fd0註冊到epfd上,將epfd上的EPOLLIN事件關聯到fd0上,詳見Linux epoll_ctl
- 新建PollArrayWrapper,部分欄位如下
pollWrapper = new PollArrayWrapper(INIT_CAP); //初始為10
pollWrapper.initInterrupt(fd0, fd1);
複製程式碼
pollArray:它的大小為
(10+1)*SIZE_POLLFD
(SIZE_POLLFD取值為8),實質是AllocatedNativeObject
- AllocatedNativeObject
NativeObject是用來操作本地記憶體的一個代理,所有的操作通過Unsafe來實現,它本身是一個單例
2: 開啟服務端socket的channel
它還是會去獲取系統級別的provider,由於已經在拿selector的時候初始化,不再新建。同樣會通過PollSelectorProvider或者是EPollSelectorProvider來開啟服務端的socket的channel,而二者的實現均是通過父類SelectorProviderImpl,建立一個ServerSocketChannelImpl例項
channel:代表與硬體、檔案、網路socket或者是程式元件等能夠進行一些I/O操作(讀和寫)的實體的連線
Closeable:是關閉與流相關的系統資源
AutoCloseable:從1.7開始的支援的語法糖try-with-resources結構,實現自動關閉資源
SelectableChannel:支援通過selector複用的Channel,提供對channel的註冊,返回對應的SelectionKey,可以工作在阻塞(預設)和非阻塞模式下
NetworkChannel:對應網路socket的channel,提供將socket繫結到本機地址的bind方法
fd是使用IOUtil.newFD建立,建立過程如下:
- 呼叫
Native方法 Net.socket0
Net.scoket0 方法對應的實現為
Net.c中的Java_sun_nio_ch_Net_socket0
,從標頭檔案的引入 #include <sys/socket.h> 可以看到,socket0的內部很多實現都依賴於作業系統本身,作業系統不一樣,就會有不同的呼叫結果。關鍵實現如下
fd = socket(domain, type, 0); setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char*)&arg,sizeof(arg)) 複製程式碼
- socket(family, type, protocol):其中family指的是要在解釋名稱時使用的地址格式(AF_INET6/AF_INET等),type指定的是通訊的語義(SOCK_STREAM/SOCK_DGRAM等),protocol執行通訊用的協議,0意味著使用預設的。它返回的就是socket file descriptor。詳見Linux socketAPI [solaris 下存在兩套實現,BSD風格socket庫-3SOCKET和 GNU/Linux軟體使用這個庫 XNET ]
- setsockop:給檔案描述符fd設定socket的選項,返回值小於0表示出了異常,詳見Linux setsocketopt
- 新建java物件FileDescriptor ,將1中返回值和新建物件一起交給IOUtil的Native方法setfdVal執行
在IOUtil.c中存在方法 Java_sun_nio_ch_IOUtil_setfdVal,它就是呼叫JNI的方法將獲取的值存入到java物件FileDescriptor中取
FileDescriptor的例項是用來表示1個開啟的檔案,或者是一個開啟的socket或者類似的位元組源
fdVal的賦值則是使用建立好的fd呼叫JNI中的(*env)->GetIntField(env, fdo, fd_fdID);
實現
3:獲取socket
本質是通過ServerSocketAdaptor建立一個例項返回
ServerSocket本質是一個對SocketImpl的包裝類,相關的請求處理都是由impl來處理 SocksSocketImpl是按照SOCKS協議的TCP socket實現,而PlainSocketImpl則是一個‘平凡’的socket實現,它不對防火牆或者代理做任何的突破。SocketImpl是所有實現socket的父抽象類,用來建立客戶端和服務端的socket
Socket類是兩臺機器之間通訊的端點,端點(endpoint)指的是 服務IP和它的埠,它的實際操作還是由SocketImpl來實現。
SOCKS4(SOCKets縮寫)是一個網路協議,它主要負責在防火牆上中繼TCP會話,以便應用使用者能夠透過防火牆進行訪問。它主要定義了兩個操作:CONNECT和BIND。
- 需要CONNECT時,客戶端傳送一個CONNECT請求給SOCKS伺服器,請求包含要連線的目的埠和目的主機等資訊,SOCKS伺服器會做一些服務許可權的校驗,驗證成功SOCKS伺服器建立與目標主機指定埠的連線(即應用伺服器),然後傳送反饋包給客戶端,反饋包通過CD的值來標識CONNECT請求的結果,CONNECT成功,SOCKS就可以在兩個方向上轉發流量了
- BIND必須發生在CONNECT之後,它實際包括一系列的步驟:1 獲取socket;2 拿到scoket對應的埠和ip地址;3 開始監聽,準備接收來自應用伺服器的呼叫 4:使用主連線通知應用伺服器它需要連線的IP地址和埠 5:接收一個來自應用伺服器的連線
SOCKS5相對於SOCKS4做了功能擴充套件,支援UDP、IPV6、鑑定的支援
4:繫結伺服器和它的埠
ServerSocketChannelImpl的bind方法。
- 看看當前channel是不是已經繫結或者關閉,如果完成,丟擲相關異常
- 看看是否有分配伺服器,沒有就隨便建一個
public InetSocketAddress(int port) {
this(InetAddress.anyLocalAddress(), port);
}
複製程式碼
- 獲取系統的SecurityManager,獲取成功,就去檢查執行緒是否有許可權來操作埠等待連線到來,不行則丟擲SecurityException
- NetHooks.beforeTcpBind ,如果使用了com.sun.sdp.conf配置,那麼將會把Tcp Socket包裝成Sdp Socket(Hello world沒有啟用)
- 執行繫結,實際執行Native方法Net.bind0,對應Net.c中的Java_sun_nio_ch_Net_bind0方法,關鍵程式碼如下
//將傳入的java物件的InetAddress和埠轉換為結構體:sockaddr_in或者sockaddr_in6
NET_InetAddressToSockaddr(env, iao, port, (struct sockaddr *)&sa, &sa_len, preferIPv6);
rv = NET_Bind(fdval(env, fdo), (struct sockaddr *)&sa, sa_len);
複製程式碼
bind對於windows系統和linux系統有不同的實現,以Linux為例,它實際執行的就是Linux bind,所做的操作就是把指定的地址(SocketAddress)分配給socket檔案描述符,
對於Hello world的實現來說就是它的欄位fd
- 監聽,實際為Linux listen,表明這個socket將會用來接收即將到來的連線請求
5:通過selector註冊channel
註冊事件在實質上就是維護新建channel的檔案描述符和SelectionKey的關係,就實現上而言, Poll用的是陣列,Epoll用的是HashMap合法的操作為SelectionKey.OP_READ、SelectionKey.OP_WRITE、SelectionKey.OP_CONNECT
6:從selector獲取任何已經註冊好併發生的事件
根據是Poll還是Epoll有不同的實現。select的實質就是去獲取poll和epoll的結果,然後更新自身維護的selector結構對應的狀態7:接收已經準備好的channel傳過來的資料
在非阻塞模式下,accept會立馬返回
Linux accept 實際上就是從監聽狀態的socketfd的連線等待佇列中獲取第一個連線請求,然後新建一個socket返回。這裡新建的SocketChannelImpl,而之前使用的是ServerSocketChannelImpl。區別在於 SocketChannelImpl支援讀寫資料,而ServerSocketChannelImpl則更多的用於等待連線的到來,充當服務端
接下來,獲取的socket方式同第3步中新建socket
8:從socket中獲取outputStream
outpusStream通過Channels.newOutputStream
新建,它會持有accept處新建的SocketChannelImpl,它實際上就是新建OutputStream並重寫它的write方法
9:回寫資料
printWriter的print經過BufferWriter到OutputStreamWriter,再到它的StreamEncoder到它的方法writeBytes
執行out.write(bb.array(), bb.arrayOffset() + pos, rem);
即socket中重寫的write方法,它的主要實現是呼叫Channels.writeFully
,然後呼叫Channel自己的SocketChannelImpl.write
方法,它核心在於n = IOUtil.write(fd, buf, -1, nd, writeLock);
static int write(FileDescriptor fd, ByteBuffer src, long position,
NativeDispatcher nd, Object lock)
throws IOException
{
//判斷是否是直接記憶體
if (src instanceof DirectBuffer)
return writeFromNativeBuffer(fd, src, position, nd, lock);
// Substitute a native buffer
int pos = src.position();
int lim = src.limit();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
//申請一個DirectBuffer,即通過ByteBuffer.allocateDirect來申請直接記憶體;
ByteBuffer bb = Util.getTemporaryDirectBuffer(rem);
try {
bb.put(src);
bb.flip();
// Do not update src until we see how many bytes were written
src.position(pos);
//寫資料,實際上執行的是FileDispatcherImpl的Native方法writ0
int n = writeFromNativeBuffer(fd, bb, position, nd, lock);
if (n > 0) {
// now update src
src.position(pos + n);
}
return n;
} finally {
Util.offerFirstTemporaryDirectBuffer(bb);
}
}
複製程式碼
可以看到這裡有一段從JVM的Buffer拷貝到NativeBuffer中,也就是說NIO的資料寫肯定是從直接記憶體傳送出去的
,如果本身不是直接記憶體則會經過一次記憶體拷貝。
JNIEXPORT jint JNICALL
Java_sun_nio_ch_FileDispatcherImpl_write0(JNIEnv *env, jclass clazz,
jobject fdo, jlong address, jint len)
{
jint fd = fdval(env, fdo);
void *buf = (void *)jlong_to_ptr(address);
return convertReturnVal(env, write(fd, buf, len), JNI_FALSE);
}
複製程式碼
最終的寫可以看到用的就是Linux write
Java NIO的本質是什麼?
為什麼一個Selector管理了多個Channel?
SelectionKey會持有各自作業系統下的SelectorImpl物件,對於PollSelectorImpl的channel註冊內部實際是通過陣列儲存了檔案描述符和Selector的關係,EpollSelectorImpl的channel註冊則是內部用的HashMap儲存檔案描述符和Selector的關係。當讀取到事件的時候,就通過輪詢的方式拿到所有準備好的事件返回,一個個的處理
NIO是如何實現的?
它依賴於作業系統本身,對於windows/mac/linux均有不同的版本實現。這裡以Liunx為例,它實際上就是個使用Linux的一系列方法,比如 read/write/accept等,操作檔案描述符
socket是什麼?
socket本身只是獲取通訊的服務和埠的一個實現類,對於服務的連線,是通過自身的屬性來處理。而這個屬性impl實際也就是對SOCKS協議的實現。來提供連線和繫結服務。
Java 阻塞IO服務端程式碼的hello world怎麼寫?
public class TimeServer {
private static Charset charset = Charset.forName("US-ASCII");
private static CharsetEncoder encoder = charset.newEncoder();
public static void main(String[] args) throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
InetSocketAddress isa = new InetSocketAddress(InetAddress.getLocalHost(), 8013);
ssc.socket().bind(isa);
for (;;)
{
SocketChannel sc = ssc.accept();
try {
String now = new Date().toString();
sc.write(encoder.encode(CharBuffer.wrap(now + "\r\n")));
System.out.println(sc.socket().getInetAddress() + " : " + now);
sc.close();
} finally {
// Make sure we close the channel (and hence the socket)
sc.close();
}
}
}
}
複製程式碼
它與NIO的區別主要區別在於在於,NIO通過configureBlocking設定為false,會把它自身的fd設定為非阻塞,而阻塞IO則沒有,預設阻塞。
Java客戶端的hello world怎麼寫?
public class TimeQuery {
// Charset and decoder for US-ASCII
private static Charset charset = Charset.forName("US-ASCII");
private static CharsetDecoder decoder = charset.newDecoder();
// Direct byte buffer for reading
private static ByteBuffer dbuf = ByteBuffer.allocateDirect(1024);
public static void main(String[] args) {
try {
InetSocketAddress isa = new InetSocketAddress(InetAddress.getLocalHost(), 8900);
SocketChannel sc = null;
try {
// Connect
sc = SocketChannel.open();
sc.connect(isa);
// Read the time from the remote host. For simplicity we assume
// that the time comes back to us in a single packet, so that we
// only need to read once.
dbuf.clear();
sc.read(dbuf);
// Print the remote address and the received time
dbuf.flip();
CharBuffer cb = decoder.decode(dbuf);
System.out.print(isa + " : " + cb);
} finally {
// Make sure we close the channel (and hence the socket)
if (sc != null)
sc.close();
}
} catch (IOException x) {
System.err.println( x);
}
}
}
複製程式碼
真實的執行實際上也就是Linux connect和Linux read
附錄
jdk 7 原始碼地址
NIO服務端 原始碼地址
IO服務端 原始碼地址
客戶端 原始碼地址
如何讀open jdk native 原始碼
java JNI簡介