网络与IO知识扫盲(三):从系统调用的角度,剖析 Socket 的连接过程、BIO 的连接过程
Socket的連接過程、TCP的一些參數
前置知識
用到的命令
netstat -natp 查看網絡連接和占用的端口
tcpdump -nn -i eth0 port 9090 開監聽抓取數據包
lsof -p <進程號>查看某個進程已經打開的文件狀態
Socket
服務端代碼
package com.bjmashibing.system.io;import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket;public class SocketIOPropertites {//server socket listen property: 這些配置不是JVM層級的,是關聯到內核的TCP協議棧的一些選項參數。private static final int RECEIVE_BUFFER = 10;private static final int SO_TIMEOUT = 0; // 服務端的超時時間private static final boolean REUSE_ADDR = false;private static final int BACK_LOG = 2; // 多少個連接可以被積壓//client socket listen property on server endpoint:private static final boolean CLI_KEEPALIVE = false;private static final boolean CLI_OOB = false;private static final int CLI_REC_BUF = 20;private static final boolean CLI_REUSE_ADDR = false;private static final int CLI_SEND_BUF = 20;private static final boolean CLI_LINGER = true;private static final int CLI_LINGER_N = 0;private static final int CLI_TIMEOUT = 0; // 客戶端的超時時間private static final boolean CLI_NO_DELAY = false; /*StandardSocketOptions.TCP_NODELAYStandardSocketOptions.SO_KEEPALIVEStandardSocketOptions.SO_LINGERStandardSocketOptions.SO_RCVBUFStandardSocketOptions.SO_SNDBUFStandardSocketOptions.SO_REUSEADDR*/public static void main(String[] args) {ServerSocket server = null;try {server = new ServerSocket();server.bind(new InetSocketAddress(9090), BACK_LOG);server.setReceiveBufferSize(RECEIVE_BUFFER);server.setReuseAddress(REUSE_ADDR);server.setSoTimeout(SO_TIMEOUT);} catch (IOException e) {e.printStackTrace();}System.out.println("server up use 9090!");try {while (true) {// System.in.read(); //分水嶺:Socket client = server.accept(); //阻塞的,沒有 -1 一直卡著不動 accept(4,System.out.println("client port: " + client.getPort());client.setKeepAlive(CLI_KEEPALIVE);client.setOOBInline(CLI_OOB);client.setReceiveBufferSize(CLI_REC_BUF);client.setReuseAddress(CLI_REUSE_ADDR);client.setSendBufferSize(CLI_SEND_BUF);client.setSoLinger(CLI_LINGER, CLI_LINGER_N);client.setSoTimeout(CLI_TIMEOUT);client.setTcpNoDelay(CLI_NO_DELAY);//client.read //阻塞 沒有 -1 0new Thread(() -> {try {InputStream in = client.getInputStream();BufferedReader reader = new BufferedReader(new InputStreamReader(in));char[] data = new char[1024];while (true) {int num = reader.read(data);if (num > 0) {System.out.println("client read some data is :" + num + " val :" + new String(data, 0, num));} else if (num == 0) {System.out.println("client readed nothing!");continue;} else {System.out.println("client readed -1...");System.in.read();client.close();break;}}} catch (IOException e) {e.printStackTrace();}}).start();}} catch (IOException e) {e.printStackTrace();} finally {try {server.close();} catch (IOException e) {e.printStackTrace();}}} }客戶端代碼
package com.bjmashibing.system.io;import java.io.*; import java.net.Socket;public class SocketClient {public static void main(String[] args) {try {Socket client = new Socket("192.168.150.11",9090);client.setSendBufferSize(20);client.setTcpNoDelay(true); // 如果數據量比較小,會不會積攢起來再發,默認是trueclient.setOOBInLine(true);OutputStream out = client.getOutputStream();InputStream in = System.in;BufferedReader reader = new BufferedReader(new InputStreamReader(in));while(true){String line = reader.readLine();if(line != null ){byte[] bb = line.getBytes();for (byte b : bb) {out.write(b);}}}} catch (IOException e) {e.printStackTrace();}} }下面詳細跟蹤建立連接的過程
啟動服務端
開啟服務端后,出現了一個對于 9090 的 listen 狀態。
TCP 三次握手是走 listen 的,建立連接之后,后面走文件描述符,那就是另外一個環節了,我們后面再講。
使用jps得到服務端的進程id號:7932
使用lsof -p 7932查看7932端口的文件描述符的分配情況。
啟動客戶端
客戶端啟動,進入代碼的阻塞等待用戶輸入邏輯
在服務端抓到了三次握手的包
在服務端看到建立了連接,雖然連接還未被使用。
在客戶端進行用戶輸入之后(服務端也有的阻塞的邏輯,需要回車才能接收client的數據)
繼續查看服務端抓包監聽
查看服務端的連接狀態:雙方開辟了資源。即便你程序不要我,我也在內核里有資源用來接收或者等待一類的。
服務端輸入回車之后
接受到了客戶端發過來的數據
剛才的socket連接已經被分配給7932了
lsof 得到了新的文件描述符 6
總結一下
TCP:面向連接的,可靠的傳輸協議
Socket:是一個四元組。ip:port ip:port四元組的任何一個元的不同,都可以區分不同的連接。
面試題 1:服務端80端口接收客戶端連接之后,是否需要為客戶端的連接分配一個隨機端口號?
答:不需要。
面試題 2:現在,有一個客戶端,有一個服務端,
客戶端的ip地址是AIP,程序使用端口號CPORT想要建立連接。
服務端的IP地址是XIP,端口號是XPORT。
現在假設某一個客戶端A開了很多連接占滿了自己的65535個端口號,那客戶端A是否還能與另一個服務端建立建立連接?
答:可以,因為只要能保證四元組唯一即可
注:一臺服務器是可以與超過65535個客戶端保持長連接的,調優到超過百萬連接都沒問題,只要四元組唯一就可以了??蛻舳藖砹酥?#xff0c;服務端是不需要單獨給它開辟一個端口號的。
下面這個圖可以說明,無論再多的連接,服務端始終是使用的同一個<ip:端口>
那么,我們常見的報錯“端口號被占用”是什么原因?
我們常見的報錯“端口號被占用”實際上是在啟動SocketSocket的時候,而不是Socket,兩者不是一個概念。如果兩個服務使用了相同的端口號,這時如果來了一個數據包,內核無法區分是哪一個服務在LISTEN,不知道要發給哪一個服務了,如下圖例子
每一個獨立的進程只要維護它自己的文件描述符唯一即可。
keepalive
三個不同層級的 keepalive
- TCP協議中規定,如果雙方建立的連接(虛無的,并不是物理的連接),如果雙方很久都不說話,你能確定對方還活著嗎?不能,因為可能突然斷電。所以規定了這么一種機制,哪怕是周期性的消耗一些網絡資源,也要及時把無效的連接踢掉,節省內存。
- HTTP級別
- 負載均衡keepalived
網絡IO的變化 演進模型(BIO)
一句話概括BIO?
BIO就是,客戶端來一個連接,拋出一個線程,來一個連接,拋出一個線程…
幾個維度
同步、異步、阻塞、非阻塞
用到的命令:
strace -ff -o out /usr/java TestSocket
用來追蹤Java程序和內核進行了哪些交互(進行了哪些系統調用)
詳細追蹤 BIO 的連接過程
TestSocket.java
用JDK1.4跑起來
在服務端用jps找到進程的id號是8384
在服務端使用tail監控out.8384文件的輸出(8384是main線程的輸出,其他的out可能是一些垃圾回收線程等其他線程的輸出)
(這里注意一下一共有8個線程,待會兒建立連接之后再看)
可以看到JVM用到了內核系統調用的accept,main線程正在阻塞
在一個客戶端上建立一個連接
在服務端我們看到,剛才阻塞 accept(3, 的位置繼續執行。34178是客戶端連接進來的隨機端口號,192.1618.150.12是來自于客戶端的ip地址
clone是linux的一個系統調用。Java當中的一個線程,就是操作系統的一個子線程。下圖我們看到,(客戶端連接進來之后),服務端調用clone函數,開啟了一個線程號為8447的新線程。flags里面記錄的是子線程共享的文件系統、打開的文件等父線程的系統資源。
下面又開始阻塞的accept
查看用strace輸出的out文件,也可以證明8447這個新線程的存在。
在服務端可以看到,多了一個文件描述符5,表示的是從node01(服務端機器名稱)到node02(客戶端機器名稱)的已連通的狀態(socket四元組)
服務端 8447.out 正在recv阻塞接收
想學好Linux,去學習文檔中這些man幫助手冊,有時候比網絡上的博客文章更準確(也可以 man man 查看幫助文檔本身的幫助文檔)
使用man 2 socket,你會發現所謂socket系統調用,其實就是調用了一個有返回值(文件描述符)的函數(用于LISTEN)
稍稍總結一下
BIO 模型的整個連接過程
無論哪種IO模型,application想要和外界通信,都要進行上面所展示的一系列的(3步)系統調用,都是不可缺少的。
之后服務端進入阻塞狀態accept(3,等待客戶端的連接。此次阻塞被成功地連接之后,又進入一的新的阻塞,等待新的客戶端連接。
一旦連接成功之后,會為這個連接拋出去一個新的線程,新的線程中又進入一個阻塞狀態recv(5,等待接收消息。
總結
以上是生活随笔為你收集整理的网络与IO知识扫盲(三):从系统调用的角度,剖析 Socket 的连接过程、BIO 的连接过程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 网络与IO知识扫盲(一):Linux虚拟
- 下一篇: 网络与IO知识扫盲(四):C10K问题、