网络与IO知识扫盲(三):从系统调用的角度,剖析 Socket 的连接过程、BIO 的连接过程
Socket的連接過程、TCP的一些參數(shù)
前置知識
用到的命令
netstat -natp 查看網(wǎng)絡(luò)連接和占用的端口
tcpdump -nn -i eth0 port 9090 開監(jiān)聽抓取數(shù)據(jù)包
lsof -p <進(jìn)程號>查看某個進(jìn)程已經(jīng)打開的文件狀態(tài)
Socket
服務(wù)端代碼
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層級的,是關(guān)聯(lián)到內(nèi)核的TCP協(xié)議棧的一些選項參數(shù)。private static final int RECEIVE_BUFFER = 10;private static final int SO_TIMEOUT = 0; // 服務(wù)端的超時時間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); // 如果數(shù)據(jù)量比較小,會不會積攢起來再發(fā),默認(rèn)是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();}} }下面詳細(xì)跟蹤建立連接的過程
啟動服務(wù)端
開啟服務(wù)端后,出現(xiàn)了一個對于 9090 的 listen 狀態(tài)。
TCP 三次握手是走 listen 的,建立連接之后,后面走文件描述符,那就是另外一個環(huán)節(jié)了,我們后面再講。
使用jps得到服務(wù)端的進(jìn)程id號:7932
使用lsof -p 7932查看7932端口的文件描述符的分配情況。
啟動客戶端
客戶端啟動,進(jìn)入代碼的阻塞等待用戶輸入邏輯
在服務(wù)端抓到了三次握手的包
在服務(wù)端看到建立了連接,雖然連接還未被使用。
在客戶端進(jìn)行用戶輸入之后(服務(wù)端也有的阻塞的邏輯,需要回車才能接收client的數(shù)據(jù))
繼續(xù)查看服務(wù)端抓包監(jiān)聽
查看服務(wù)端的連接狀態(tài):雙方開辟了資源。即便你程序不要我,我也在內(nèi)核里有資源用來接收或者等待一類的。
服務(wù)端輸入回車之后
接受到了客戶端發(fā)過來的數(shù)據(jù)
剛才的socket連接已經(jīng)被分配給7932了
lsof 得到了新的文件描述符 6
總結(jié)一下
TCP:面向連接的,可靠的傳輸協(xié)議
Socket:是一個四元組。ip:port ip:port四元組的任何一個元的不同,都可以區(qū)分不同的連接。
面試題 1:服務(wù)端80端口接收客戶端連接之后,是否需要為客戶端的連接分配一個隨機(jī)端口號?
答:不需要。
面試題 2:現(xiàn)在,有一個客戶端,有一個服務(wù)端,
客戶端的ip地址是AIP,程序使用端口號CPORT想要建立連接。
服務(wù)端的IP地址是XIP,端口號是XPORT。
現(xiàn)在假設(shè)某一個客戶端A開了很多連接占滿了自己的65535個端口號,那客戶端A是否還能與另一個服務(wù)端建立建立連接?
答:可以,因為只要能保證四元組唯一即可
注:一臺服務(wù)器是可以與超過65535個客戶端保持長連接的,調(diào)優(yōu)到超過百萬連接都沒問題,只要四元組唯一就可以了。客戶端來了之后,服務(wù)端是不需要單獨給它開辟一個端口號的。
下面這個圖可以說明,無論再多的連接,服務(wù)端始終是使用的同一個<ip:端口>
那么,我們常見的報錯“端口號被占用”是什么原因?
我們常見的報錯“端口號被占用”實際上是在啟動SocketSocket的時候,而不是Socket,兩者不是一個概念。如果兩個服務(wù)使用了相同的端口號,這時如果來了一個數(shù)據(jù)包,內(nèi)核無法區(qū)分是哪一個服務(wù)在LISTEN,不知道要發(fā)給哪一個服務(wù)了,如下圖例子
每一個獨立的進(jìn)程只要維護(hù)它自己的文件描述符唯一即可。
keepalive
三個不同層級的 keepalive
- TCP協(xié)議中規(guī)定,如果雙方建立的連接(虛無的,并不是物理的連接),如果雙方很久都不說話,你能確定對方還活著嗎?不能,因為可能突然斷電。所以規(guī)定了這么一種機(jī)制,哪怕是周期性的消耗一些網(wǎng)絡(luò)資源,也要及時把無效的連接踢掉,節(jié)省內(nèi)存。
- HTTP級別
- 負(fù)載均衡keepalived
網(wǎng)絡(luò)IO的變化 演進(jìn)模型(BIO)
一句話概括BIO?
BIO就是,客戶端來一個連接,拋出一個線程,來一個連接,拋出一個線程…
幾個維度
同步、異步、阻塞、非阻塞
用到的命令:
strace -ff -o out /usr/java TestSocket
用來追蹤Java程序和內(nèi)核進(jìn)行了哪些交互(進(jìn)行了哪些系統(tǒng)調(diào)用)
詳細(xì)追蹤 BIO 的連接過程
TestSocket.java
用JDK1.4跑起來
在服務(wù)端用jps找到進(jìn)程的id號是8384
在服務(wù)端使用tail監(jiān)控out.8384文件的輸出(8384是main線程的輸出,其他的out可能是一些垃圾回收線程等其他線程的輸出)
(這里注意一下一共有8個線程,待會兒建立連接之后再看)
可以看到JVM用到了內(nèi)核系統(tǒng)調(diào)用的accept,main線程正在阻塞
在一個客戶端上建立一個連接
在服務(wù)端我們看到,剛才阻塞 accept(3, 的位置繼續(xù)執(zhí)行。34178是客戶端連接進(jìn)來的隨機(jī)端口號,192.1618.150.12是來自于客戶端的ip地址
clone是linux的一個系統(tǒng)調(diào)用。Java當(dāng)中的一個線程,就是操作系統(tǒng)的一個子線程。下圖我們看到,(客戶端連接進(jìn)來之后),服務(wù)端調(diào)用clone函數(shù),開啟了一個線程號為8447的新線程。flags里面記錄的是子線程共享的文件系統(tǒng)、打開的文件等父線程的系統(tǒng)資源。
下面又開始阻塞的accept
查看用strace輸出的out文件,也可以證明8447這個新線程的存在。
在服務(wù)端可以看到,多了一個文件描述符5,表示的是從node01(服務(wù)端機(jī)器名稱)到node02(客戶端機(jī)器名稱)的已連通的狀態(tài)(socket四元組)
服務(wù)端 8447.out 正在recv阻塞接收
想學(xué)好Linux,去學(xué)習(xí)文檔中這些man幫助手冊,有時候比網(wǎng)絡(luò)上的博客文章更準(zhǔn)確(也可以 man man 查看幫助文檔本身的幫助文檔)
使用man 2 socket,你會發(fā)現(xiàn)所謂socket系統(tǒng)調(diào)用,其實就是調(diào)用了一個有返回值(文件描述符)的函數(shù)(用于LISTEN)
稍稍總結(jié)一下
BIO 模型的整個連接過程
無論哪種IO模型,application想要和外界通信,都要進(jìn)行上面所展示的一系列的(3步)系統(tǒng)調(diào)用,都是不可缺少的。
之后服務(wù)端進(jìn)入阻塞狀態(tài)accept(3,等待客戶端的連接。此次阻塞被成功地連接之后,又進(jìn)入一的新的阻塞,等待新的客戶端連接。
一旦連接成功之后,會為這個連接拋出去一個新的線程,新的線程中又進(jìn)入一個阻塞狀態(tài)recv(5,等待接收消息。
總結(jié)
以上是生活随笔為你收集整理的网络与IO知识扫盲(三):从系统调用的角度,剖析 Socket 的连接过程、BIO 的连接过程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 网络与IO知识扫盲(一):Linux虚拟
- 下一篇: Windows 配置 Github 的