1. 計算機網絡編程基礎
1.七層模型
七層模型(OSI,Open System Interconnection參考模型),是參考是國際標准化組織制定的一個用於計算機或通信系統間互聯的標准體系。它是一個七層抽象的模型,不僅包括一系列抽象的術語和概念,也包括具體的協議。 經典的描述如下:
簡述每一層的含義:
- 物理層(Physical Layer):建立、維護、斷開物理連接。
- 數據鏈路層 (Link):邏輯連接、進行硬件地址尋址、差錯校驗等。
- 網絡層 (Network):進行邏輯尋址,實現不同網絡之間的路徑選擇。
- 傳輸層 (Transport):定義傳輸數據的協議端口號,及流控和差錯校驗。
- 會話層(Session Layer):建立、管理、終止會話。
- 表示層(Presentation Layer):數據的表示、安全、壓縮。
- 應用層 (Application):網絡服務與最終用戶的一個接口
每一層利用下一層提供的服務與對等層通信,每一層使用自己的協議。了解了這些,然並卵。但是,這一模型確實是絕大多數網絡編程的基礎,作為抽象類存在的,而TCP/IP協議棧只是這一模型的一個具體實現。
2.TCP/IP協議模型
IP數據包結構:

TCP數據包結構:

一個模型例子:
尋址過程:每台機子都有個物理地址MAC地址和邏輯地址IP地址,物理地址用於底層的硬件的通信,邏輯地址用於上層的協議間的通信。尋址過程會先使用ip地址進行路由尋址,在不同網絡中進行路由轉發,到了同一個局域網時,再根據物理地址進行廣播尋址,數據在以太網的局域網中都是以廣播方式傳輸的,整個局域網中的所有節點都會收到該幀,只有目標MAC地址與自己的MAC地址相同的幀才會被接收。
建立可靠的連接:A向B傳輸一個文件時,如果文件中有部分數據丟失,就可能會造成在B上無法正常閱讀或使用。 TCP協議就是建立了可靠的連接:
TCP三次握手確定了雙方數據包的序號、最大接受數據的大小(window)以及MSS(Maximum Segment Size)
會話層用來建立、維護、管理應用程序之間的會話,主要功能是對話控制和同步,編程中所涉及的session是會話層的具體體現。表示層完成數據的解編碼,加解密,壓縮解壓縮等。
2.Socket編程
在Linux世界,“一切皆文件”,操作系統把網絡讀寫作為IO操作,就像讀寫文件那樣,對外提供出來的編程接口就是Socket。所以,socket(套接字)是通信的基石,是支持TCP/IP協議網絡通信的基本操作單元。socket實質上提供了進程通信的端點。進程通信之前,雙方首先必須各自創建一個端點,否則是沒有辦法建立聯系並相互通信的。一個完整的socket有一個本地唯一的socket號,這是由操作系統分配的。
在許多操作系統中,Socket描述符和其他IO描述符是集成在一起的,操作系統把socket描述符實現為一個指針數組,這些指針指向內部數據結構。進程進行Socket操作時,也有着多種處理方式,如阻塞式IO,非阻塞式IO,多路復用(select/poll/epoll),AIO等等。
多路復用往往在提升性能方面有着重要的作用。
當前主流的Server側Socket實現大都采用了epoll的方式,例如Nginx, 在配置文件可以顯式地看到 use epoll。
舉個栗子
Java中Socket服務端的簡單實現:基本思路就是一個大循環不斷監聽客戶端請求,為了提高處理效率可以使用線程池多個線程進行每個連接的數據讀取
public class BIOServer {
private ServerSocket serverSocket;
private ExecutorService executorService = Executors.newCachedThreadPool();
class Handler implements Runnable {
Socket socket;
public Handler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
BufferedReader buf = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String readData = buf.readLine();
while (readData != null) {
readData = buf.readLine();
System.out.println(readData);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public BIOServer(int port) {
try {
serverSocket = new ServerSocket(port);
} catch (IOException e) {
e.printStackTrace();
}
}
public void run() {
try {
Socket socket = serverSocket.accept();
executorService.submit(new Handler(socket));
} catch (Exception e) {
}
}
}
客戶端:建立socket連接、發起請求、讀取響應
public class IOClient {
public void start(String host, int port) {
try {
Socket s = new Socket("127.0.0.1",8888);
InputStream is = s.getInputStream();
OutputStream os = s.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
bw.write("測試客戶端和服務器通信,服務器接收到消息返回到客戶端\n");
bw.flush();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String mess = br.readLine();
System.out.println("服務器:"+mess);
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.IO模型
對於一次IO訪問(以read舉例),數據會先被拷貝到操作系統內核的緩沖區page cache中,然后才會從操作系統內核的緩沖區拷貝到應用程序的地址空間。所以說,當一個read操作發生時,它會經歷兩個階段:
- 等待數據准備
- 將數據從內核拷貝到進程中
IO模型的分類有下:
- 阻塞 I/O(blocking IO)
- 非阻塞 I/O(nonblocking IO)
- I/O 多路復用( IO multiplexing)
- 異步 I/O(asynchronous IO)
BIO 阻塞 I/O
缺點:一個請求一個線程,浪費線程,且上下文切換開銷大;
上面寫的socket列子就是典型的BIO

當用戶進程調用了recvfrom這個系統調用,kernel就開始了IO的第一個階段:准備數據(對於網絡IO來說,很多時候數據在一開始還沒有到達。比如,還沒有收到一個完整的UDP包。這個時候kernel就要等待足夠的數據到來)。這個過程需要等待,也就是說數據被拷貝到操作系統內核的緩沖區中是需要一個過程的。而在用戶進程這邊,整個進程會被阻塞(當然,是進程自己選擇的阻塞)。當kernel一直等到數據准備好了,它就會將數據從kernel中拷貝到用戶內存,然后kernel返回結果,用戶進程才解除block的狀態,重新運行起來。
NIO 非阻塞 I/O

當用戶進程發出read操作時,如果kernel中的數據還沒有准備好,那么它並不會block用戶進程,而是立刻返回一個error 。從用戶進程角度講 ,它發起一個read操作后,並不需要等待,而是馬上就得到了一個結果。用戶進程判斷結果是一個error時,它就知道數據還沒有准備好,於是它可以再次發送read操作。一旦kernel中的數據准備好了,並且又再次收到了用戶進程的system call,那么它馬上就將數據拷貝到了用戶內存,然后返回。
nonblocking IO的特點是用戶進程需要不斷的主動詢問kernel數據好了沒有。
I/O 多路復用
IO multiplexing就是我們說的select,poll,epoll,有些地方也稱這種IO方式為event driven IO。select/epoll的好處就在於單個process就可以同時處理多個網絡連接的IO。它的基本原理就是select,poll,epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程。
機制:一個線程以阻塞的方式監聽客戶端請求;另一個線程采用NIO的形式select已經接收到數據的channel信道,處理請求;
- select,poll,epoll模型 - 處理更多的連接

上面所說的多路復用的select,poll,epoll本質上都是同步IO,因為他們都需要在讀寫事件就緒后自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,實際上是指阻塞在select上面,必須等到讀就緒、寫就緒等網絡事件。異步IO則無需自己負責進行讀寫,異步IO的實現會負責把數據從內核拷貝到用戶空間。
I/O 多路復用的特點是通過一種機制一個進程能同時等待多個文件描述符,
而這些文件描述符(套接字描述符)其中的任意一個進入讀就緒狀態,select()
函數就可以返回。所以,如果處理的連接數不是很高的話,使用select/epoll的web
server不一定比使用multi-threading + blocking IO的web
server性能更好,可能延遲還更大。select/
epoll的優勢並不是對於單個連接能處理得更快,而是在於能處理更多的連接。
- 一個面試問題:select、poll、epoll的區別?
Java中的I/O 多路復用: Reactor模型
(主從Reactor模型)netty就是主從Reactor模型的實現,相當於這個模型在

對比與傳統的I/O 多路復用,Reactor模型增加了事件分發器,基於事件驅動,能夠將相應的讀寫事件分發給不同的線程執行,真正實現了非阻塞I/O。
基於Reactor Pattern 處理模式中,定義以下三種角色
- Reactor將I/O事件分派給對應的Handler
- Acceptor處理客戶端新連接,並分派請求到處理器鏈中
- Handlers執行非阻塞讀/寫 任務
舉個栗子
回顧我們上面寫的代碼,是不是每個線程處理一個連接,顯然在高並發情況下是不適用的,應該采用 IO多路復用 的思想,使得一個線程能夠處理多個連接,並且不能阻塞讀寫操作,添加一個 選擇器在buffer有數據的時候就開始寫入用戶空間.這里的多路是指N個連接,每一個連接對應一個channel,或者說多路就是多個channel。復用,是指多個連接復用了一個線程或者少量線程
現在我們來優化下上面的socket IO模型
優化后的IO模型:
實現一個最簡單的Reactor模式:注冊所有感興趣的事件處理器,單線程輪詢選擇就緒事件,執行事件處理器。流程就是不斷輪詢可以進行處理的事件,然后交給不同的handler進行處理.
上面提到的主要是四個網絡事件:有連接就緒,接收就緒,讀就緒,寫就緒。I/O復用主要是通過 Selector復用器來實現的,可以結合下面這個圖理解上面的敘述

public class NIOServer {
private ServerSocketChannel serverSocket;
private Selector selector;
private ReadHandler readHandler;
private WriteHandler writeHandler;
private ExecutorService executorService = Executors.newCachedThreadPool();
abstract class Handler {
protected SelectionKey key;
}
class ReadHandler extends Handler implements Runnable {
@Override
public void run() {
///...讀操作
}
}
class WriteHandler extends Handler implements Runnable {
@Override
public void run() {
///...寫操作
}
}
public NIOServer(int port) {
try {
selector = Selector.open();
serverSocket = ServerSocketChannel.open();
serverSocket.bind(new InetSocketAddress(port));
serverSocket.register(this.selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void run() {
while (!Thread.interrupted()) {
try {
selector.select(); //阻塞等待事件
Iterator<SelectionKey> iterator = this.selector.keys().iterator(); // 事件列表 , key -> channel ,每個KEY對應了一個channel
while (iterator.hasNext()) {
iterator.remove();
dispatch(iterator.next()); //分發事件
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void dispatch(SelectionKey key) {
if (key.isAcceptable()) {
register(key); //新連接建立,注冊一個新的讀寫處理器
} else if (key.isReadable()) {
this.executorService.submit(new ReadHandler(key)); //可以寫,執行寫事件
} else if (key.isWritable()) {
this.executorService.submit(new WriteHandler(key)); //可以讀。執行讀事件
}
}
private void register(SelectionKey key) {
ServerSocketChannel channel = (ServerSocketChannel) key.channel(); //通過key找到對應的channel
try {
SocketChannel socketChannel = channel.accept();
channel.configureBlocking(false);
channel.register(this.selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
}
優化線程模型
上述模型還可以繼續優化。因為上述模型只是增多個客戶端連接的數量,但是在高並發的情況下,
