本文主要分析從Java Socket API到Linux Socket API的調用鏈,從而來探究Java Socket是如何利用Linux提供的系統調用來實現對應功能的。
Java Socket API示例代碼
首先給出一個利用Java Socket API編寫的簡易的Hello/Hi代碼示例。
//服務端
1 ServerSocket server = new ServerSocket(8000); 2 Socket client = server.accept(); 3 InputStream in = client.getInputStream(); 4 byte[] bytes = new byte[1024]; 5 int len = in.read(bytes); 6 String data = new String(bytes, 0 , len); 7 System.out.println("接收客戶端消息:" + data); 8 9 OutputStream out = client.getOutputStream(); 10 out.write("Hi".getBytes()); 11 client.close();
//客戶端
1 Socket client = new Socket("localhost", 8000); 2 OutputStream out = client.getOutputStream(); 3 String msg = "Hello"; 4 out.write(msg.getBytes()); 5 6 InputStream in = client.getInputStream(); 7 byte[] bytes = new byte[1024]; 8 int len = in.read(bytes); 9 String s = new String(bytes, 0, len); 10 System.out.println("接收服務端響應信息:" + s); 11 client.close();
服務端創建及接收連接
Socket服務端通過調用ServerSocket構造函數來進行創建並調用accept來接收客戶端的連接請求。
創建
通過源碼調試我們可以發現,在ServerSocket的構造函數中,調用了ServerSocket的bind方法。在bind方法中獲取了AbstractPlainSocketImpl對象並調用了該對象兩個重要的方法bind和listen。分別在AbstractPlainSocketImpl的bind和listen方法中,調用到了native方法socketBind和socketListen。為了搞清楚這兩個native方法到底使用了哪些個Linux系統調用來實現功能,我們繼續查閱了jdk中提供的native方法源碼,在native/java/net/PlainSocketImpl.c中可以找到方法Java_java_net_PlainSocketImpl_socketBind和Java_java_net_PlainSocketImpl_socketListen。在方法Java_java_net_PlainSocketImpl_socketBind中可以清楚看到調用了NET_Bind來進行綁定,而NET_Bind實現在native/java/net/net_util_md.c,最終通過系統調用bind來進行綁定。在方法Java_java_net_PlainSocketImpl_socketListen中也可以看到調用了JVM_Listen來進行端口監聽,而JVM_Listen實現在jvm.c,簡單地調用了listen系統調用來進行監聽。下圖為服務端創建過程的大致調用流程。

連接接收
serverSocket調用accept來進行等待接收連接。其內部調用鏈與創建類似,即在implAccept方法調用過程中,獲取了AbstractPlainSocketImpl對象並調用了該對象的accept方法,在AbstractPlainSocketImpl的accept方法中,進一步調用了native方法socketAccept。在native/java/net/PlainSocketImpl.c中可以找到該native方法的具體實現為Java_java_net_PlainSocketImpl_socketAccept,其中進一步調用了linux_close.c中的NET_Accept方法,並最終調用了系統調用accept來進行等待接收連接。
客戶端創建
客戶端的創建與服務端的創建相比較為簡單。在Socket的構造函數中,如果我們指定了本地的InetAdress和localPort則會先調用bind方法來對指定端口進行綁定,具體bind過程與上述服務端bind過程類似。但是通常我們並不會客戶端的本地地址進行指定,所以並不會執行bind過程,而是直接進行connect過程。在Socket的connect方法中,首先也會獲取AbstractPlainSocketImpl對象並調用該對象的connect,然后調用到native方法socketConnect。按照上述的jdk native分析過程,我們可以查到socketConnect的調用鏈(括號外為方法,括號內為文件),Java_java_net_PlainSocketImpl_socketConnect(PlainSocketImpl.c)->NET_Select(linux_close.c)->select(sys_call)
數據讀寫
不管是客戶端還是服務端都可能會需要進行數據的讀寫,其實現方式是相同的。
數據讀
Java Socket直接通過獲取SocketInputStream,調用read方法來進行數據讀入。read方法進而調用了socketRead,在socketRead方法中又調用了native方法socketRead0。native方法socketRead0的實際實現為native/java/net/SocketInputStream.c中的Java_java_net_SocketInputStream_socketRead0,該方法繼而又調用了linux_close.c中的NET_Read,最終調用了系統調用recv來進行數據讀入。
數據寫
Java Socket直接通過獲取SocketOutputStream,調用write方法來進行數據寫入。write方法進而調用了socketWrite,在socketWrite方法中又調用了native方法socketWrite0。native方法socketWrite0的實際實現為native/java/net/SocketOutputStream.c中的Java_java_net_SocketOutputStream_socketWrite0,該方法繼而又調用了linux_close.c中的NET_Send,最終調用了系統調用send來進行數據寫入。
關閉
當通訊雙方通訊結束時,需要關閉socket。在Java代碼中可以直接調用Socket的close來進行關閉。Socket的close方法會進一步調用AbstractPlainSocketImpl的close方法,繼而調用native方法socketClose0。可在jdk提供的native源碼中找到該native方法的調用鏈,Java_java_net_PlainSocketImpl_socketClose0(PlainSocketImpl.c)->NET_SocketClose(linux_close.c)->closefd(sys_call).
