老規矩,帶着問題閱讀:node
咱們先來回顧一下,咱們編寫一個網絡程序有哪些步驟? 基於socket的編程:linux
代碼以下:算法
public class Server { public static void main(String[] args) throws Exception { //建立一個socket套接字,開始監聽某個端口 對應了 socket() bind() listen() ServerSocket serverSocket = new ServerSocket(8080); // (1) 接收新鏈接線程 new Thread(() -> { while (true) { try { // 等待客戶端鏈接,accept() 獲取一個新鏈接 Socket socket = serverSocket.accept(); new Thread(() -> { try { byte[] data = new byte[1024]; InputStream inputStream = socket.getInputStream(); while (true) { int len; // 讀取字節數組 對應read() while ((len = inputStream.read(data)) != -1) { System.out.println(new String(data, 0, len)); } } } catch (IOException e) { } }).start(); } catch (IOException e) {} } }).start(); } } public class Client { public static void main(String[] args) { try { //對應 socket() 和 connect() 發起鏈接 Socket socket = new Socket("127.0.0.1", 8000); while (true) { try { //對應 write() 方法 socket.getOutputStream().write((new Date() + ": hello world").getBytes()); socket.getOutputStream().flush(); Thread.sleep(2000); } catch (Exception e) { } } } catch (IOException e) { } } }
服務端咱們首先會建立一個監聽套接字,而後給這個套接字綁定一個ip和端口,這一步對應的方法就是bind(),以後就是調用listen()來監聽端口,端口是和應用程序對應的,網卡收到一個數據包的時候後須要知道這個包是給哪一個程序用的,固然一個應用程序能夠監聽多個端口。以後客戶端發起鏈接內核會分配一個隨機端口,而後tcp在經歷三次握手成功後,客戶端會建立一個套接字由connect()方法返回,而服務端的accept()方法也會返回一個套接字,以後雙方都會基於這個套接字進行讀寫操做。因此服務端會維護兩種類型的套接字,一種用於監聽,另外一種用於和客戶端進行讀寫。編程
而在linux內核中,socket實際上是一個文件,掛載於SocketFS文件類型下,有點相似於/proc,不過該文件不能像磁盤上的文件同樣進行正常的訪問和讀寫。既然是文件,就會有inode來表示索引,有具體的地方存儲數據不論是磁盤仍是內存,而socket的數據是存儲在內存中的,每一個報文的數據是存放在一個叫 sk_buff 的結構體裏,要訪問文件咱們通常會對應一個文件描述符,每一個文件描述符都會有一個id,在jdk中也有相關定義。數組
public final class FileDescriptor { private int fd;
jvm啓動後就是一個獨立進程,每一個進程會維護一個數組,這個數組存放該進程已經打開的文件的描述符,數組前三個分別是標準輸入,標準輸出,錯誤輸出三個文件描述符,從第4個開始爲用戶打開的文件,或者建立的socket,而數組的下標就是文件描述符的id,內核經過文件描述符能夠找到對應的inode,而後在經過vfs找到對應的文件,進行read和write操做。緩存
linux內核中會維護兩個隊列,這兩個隊列的長度都是有限制且能夠配置的,當客戶端發起connect()請求後,服務端收到syn包後將該信息放入sync隊列,以後客戶端回覆ack後從sync隊列取出,放到accept隊列,以後服務端調用accept()方法會從accept隊列取出生成socket。網絡
若是客戶端發起sync請求,可是不回覆ack,將致使sync隊列滿載,以後會拒接新的鏈接。若是客戶端發起ack請求後,服務端一直不調用,或者調用accept隊列太慢,將致使accept隊列滿載,accept隊列滿了則收到ack後沒法從syn隊列移出去,致使syn隊列也會堆積,最終拒絕鏈接。因此服務端通常會將accept單獨起一個線程執行,避免accept太慢致使數據丟棄。固然accept()方法也有阻塞和非阻塞兩種,當accept隊列爲空的時候阻塞方法會一直等待,非阻塞方法會直接返回一個錯誤碼。數據結構
鏈接創建好後,客戶端和服務端都有一個socket套接字,雙方均可以經過各自的套接字進行發送和接收消息,socket裏面維護了兩個隊列,一個發送隊列,一個接收隊列。併發
發送的時候數據在用戶空間的內存中,當調用send()或者write()方法的時候,會將待發送的數據按照MSS進行拆分,而後將拆分好的數據包拷貝到內核空間的發送隊列,這個隊列裏面存放的是全部已經發送的數據包,對應的數據結構就是sk_buff,每個數據包也就是sk_buff都有一個序號,以及一個狀態,只有當服務端返回ack的時候,纔會把狀態改成發送成功,而且會將這個ack報文的序號以前的報文都確認掉,若是長期沒有確認,會從新調用tcp_push繼續發送,若是發送隊列慢了,則從用戶空間拷貝到內核空間的操做就會阻塞,並觸發清理隊列中已確認發送成功的數據包。tcp層會將數據包加上ip頭而後發給ip層處理,ip層將數據包加入到一個qdisc隊列,網卡驅動程序檢測到qdisc隊列有數據就會調用DMA Engine將sk_buff拷貝到網卡併發送出去,網卡驅動經過ringbuffer來指向內核中的數據,因此qdisc的長度也會影響到網絡發送的吞吐量。jvm
關於mss分片:mtu是數據鏈路層的最大傳輸單元,通常爲1500字節,而一個ip包的最大長度爲65535,因此ip層在發送數據前會根據mtu分片,這樣一個tcp包原本對應一個ip包,分片後將對應多個ip包,每一個包都有一個ip頭,在接收端須要等到全部的ip包到達後,才能肯定這個tcp收到而後才發送ack,這種方式無疑是低效的,因此tcp層會盡可能阻止ip層進行分片,他會在從用戶空間拷貝的時候就會按照mtu進行拆分,將一個數據包拆分紅多個數據包。可是鏈路中mtu是會改變的,爲了徹底避免ip層進行分片,能夠在ip層設置一個df標記,若是必定要分片就慧慧一個icmp報文。
關於流控:
因爲tcp發送的時候會進行各類分片和合並,因此接收方會出現粘包現象,須要應用層進行處理。
當服務端網卡收到一個報文後,網卡驅動調用DMA engine將數據包經過ringbuffer拷貝到內核緩衝區中,拷貝成功後,發起中斷通知中斷處理程序,這時候ip層會處理該數據包,以後交給tcp層,最終到達tcp層的recv buffer(接收隊列),這時候就會返回ack給客戶端,並無等到客戶端調用read將數據從內核拷貝到用戶空間,因此應用層也應該有相關的確認機制。若是recv buffer設置的過小,或者應用層一直不來取,那麼也將阻塞數據接收,從而影響到滑動窗口大小,致使吞吐量下降。
tcp在收到數據包後會獲取序號,而且看是否應該正好放入接收隊列,若是此時收到一個大序號的報文,會將該報文緩存直到接收隊列中以前的報文已經插入。
另外若是網卡支持多隊列,能夠將多個隊列綁定到不一樣的cpu上,這樣網卡收到報文後,不一樣的隊列就會經過中斷觸發不一樣的cpu,從而能夠提升吞吐量。
c10k問題是指怎麼支持單機1萬的併發請求,咱們想到經過select的多路複用模式,用一個單獨的線程去掃描須要監聽的文件描述符,若是這些文件描述符裏面有可讀或者可寫的就返回(tcp層在收到報文拷貝到內存後會修改這個文件描述符的狀態),沒有就阻塞,不過這種方式須要對文件描述符進行掃描,效率不高。而epoll方式採用紅黑樹去管理文件描述符,當文件可讀或者可寫的時候會經過一個回調函數通知用戶進行具體的io操做。