網絡編程之BIO/NIO基礎
什么是網絡編程
網絡編程是指編寫運行在多個設備上(計算機)的程序, 通過網絡進行數據交換. 比如現在流行的微服務, 把一個大的系統按照功能拆分多個微服務, 每個微服務都是一個獨立的應用, 部署在不同的服務器上, 不同服務器上的微服務如何進行通信就是屬于網絡編程的范疇.
TCP、IP、HTTP、Socket的區別
網絡模型(OSI)從下往上分為七層, 分別是物理層、數據鏈路層、網絡層、傳輸層、會話層、表示層和應用層. IP協議是屬于網絡層的協議, TCP是屬于傳輸層的協議, HTTP是屬于應用層的協議, Socket則是對TCP/IP協議的封裝和應用.
網絡編程三要素
網絡編程三要素分別是IP、端口號、TCP/UDP協議. IP是每個設備在網絡中的唯一標識, 端口號是每個程序在設備上的唯一標識, TCP/UDP是數據傳輸的協議.
(1)TCP協議和UDP協議的區別
- TCP協議是面向連接的(三次握手), 數據安全, 速度慢.
- UDP協議是面向無連接的, 數據不安全, 速度快.
(2)TCP協議的三次握手
- 第一次握手:客戶端發送syn包(syn=j)到服務器,并進入SYN_SEND狀態,等待服務器確認;SYN:同步序列編號(Synchronize Sequence Numbers).
- 第二次握手:服務器收到syn包,必須確認客戶的SYN(ack=j+1),同時自己也發送一個SYN包(syn=k),即SYN+ACK包,此時服務器進入SYN_RECV狀態;
- 第三次握手:客戶端收到服務器的SYN+ACK包,向服務器發送確認包ACK(ack=k+1),此包發送完畢,客戶端和服務器進入ESTABLISHED狀態,完成三次握手。
握手過程中傳送的包里不包含數據,三次握手完畢后,客戶端與服務器才正式開始傳送數據。理想狀態下,TCP連接一旦建立,在通信雙方中的任何一方主動關閉連接之前,TCP 連接都將被一直保持下去。斷開連接時服務器和客戶端均可以主動發起斷開TCP連接的請求,斷開過程需要經過“四次揮手”.
(3)TCP協議的四次揮手
- 第一次揮手:當主機A的應用程序通知TCP數據已經發送完畢時,TCP向主機B發送一個帶有FIN附加標記的報文段(FIN表示英文finish)。
- 第二次揮手:主機B收到這個FIN報文段之后,并不立即用FIN報文段回復主機A,而是先向主機A發送一個確認序號ACK,同時通知自己相應的應用程序:對方要求關閉連接. (先發送ACK的目的是為了防止在這段時間內,對方重傳FIN報文段)
- 第三次揮手:主機B的應用程序告訴TCP:我要徹底的關閉連接,TCP向主機A送一個FIN報文段。
- 第四次揮手:主機A收到這個FIN報文段后,向主機B發送一個ACK表示連接徹底釋放。
由于TCP連接是雙向的, 因此每個方向都必須單獨進行關閉. 當一方完成它的數據發送任務后就會發送一個FIN來終止這個方向的連接, 收到一個FIN只意味著這一方向上沒有數據流動, 一個TCP連接在收到一個FIN后仍能發送數據. 首先進行關閉的一方將執行主動關閉, 而另一方執行被動關閉.
(4)為什么建立連接是三次握手, 而關閉連接卻是四次握手呢?
這是因為服務端LISTEN狀態下的SOCKET當收到SYN報文的連接請求后, 它可以把ACK和SYN放在一個報文里發送. 但關閉連接時, 當收到客戶端的FIN報文通知時, 它僅僅表示客戶端沒有數據發送給服務端, 但未必服務端的數據都發送給客戶端了, 所以服務端不會馬上關閉SOCKET連接. 當服務端數據發送完畢后, 會發送給客戶端一個FIN報文, 表示同意關閉連接, 所以這里的ACK報文和FIN報文是分開發送的.
BIO(Blocking IO)
BIO也叫同步阻塞IO, 對于每一個客戶端的連接請求都會創建一個新線程來進行處理, 處理完成后線程銷毀. 當一個線程調用IO流讀寫數據時,該線程被阻塞,直到讀到數據,或數據完全寫入, 該線程在此期間不能再干任何事情。
public class Server {
public static void main(String[] args) {
ServerSocket server = null;
try {
server = new ServerSocket(12000);
System.out.println("server start...");
while(true){
//進行阻塞,監聽端口
Socket socket = server.accept();
//新建一個線程執行客戶端的任務
new Thread(new ServerHandler(socket)).start();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(server != null){
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
server = null;
}
}
}
public class ServerHandler implements Runnable{
private Socket socket ;
public ServerHandler(Socket socket){
this.socket = socket;
}
@Override
public void run() {
BufferedReader in = null;
PrintWriter out = null;
try {
in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
out = new PrintWriter(this.socket.getOutputStream(), true);
String content = null;
while(true){
content = in.readLine();
if(content == null) break;
System.out.println("Server :" + content);
out.println("Server response");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(in != null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(out != null){
try {
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
socket = null;
}
}
}
public class Client {
public static void main(String[] args) {
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try {
socket = new Socket("127.0.0.1", 12000);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
//向服務器端發送數據
out.println("Client request");
String response = in.readLine();
System.out.println("Client: " + response);
} catch (Exception e) {
e.printStackTrace();
} finally {
if(in != null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(out != null){
try {
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
socket = null;
}
}
}
偽異步IO
使用線程池來管理線程, 實現1個或多個線程處理N個客戶端.
public class Server {
public static void main(String[] args) {
ServerSocket server = null;
try {
server = new ServerSocket(12000);
System.out.println("server start...");
while(true){
//進行阻塞,監聽端口
Socket socket = server.accept();
//用線程池來管理線程
ThreadPoolExecutor executor = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(),
10,
120L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(20)
);
//新建一個線程執行客戶端的任務
executor.execute(new Thread(new ServerHandler(socket)));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(server != null){
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
server = null;
}
}
}
NIO(Non-Blocking IO)
NIO也叫同步非阻塞IO, NIO的服務端可以只啟動一個專門的線程來處理所有的 IO 事件, 且不會被任何IO事件阻塞住.
服務端和客戶端各自維護一個Selector選擇器, Selector會不斷地輪詢注冊在其上的通道(Channel)是否發生事件, 只有事件發生時才會去執行相應的操作. 當客戶端連接服務器時發生OP_CONNECT事件, 當服務端接收到客戶端連接時發生OP_ACCEPT事件, 當有數據發送過來時發生OP_READ事件, 當要發送數據給對方時發生OP_WRITE事件.
Buffer
在BIO中, 數據直接讀寫到Stream對象中. 而在NIO中, 所有數據都是讀寫到Buffer中, 然后通過Channel傳輸. Buffer實質上是一個數組, 通常它是一個字節數組(ByteBuffer) ,也可以是其他類型的數組.
(1)成員變量
- mark : s初始值為-1,用于備份當前的position;
- position : 初始值為0, position表示當前可以寫入或讀取數據的位置,當寫入或讀取一個數據后,position移動到下一個位置
- limit : 寫模式下,limit表示最多能往Buffer里寫多少數據,等于capacity值;讀模式下,limit表示最多可以讀取多少數據
- capacity : 緩存數組大小
(2)成員方法
- allocate(int capacity) : 創建指定長度的緩沖區
- put(E e) : 添加一個元素
- get() : 獲取第一個元素
- wrap(E[] array) : 將數組元素添加到緩沖中
- clear() : 清除數據, 實際上數據沒有被清除.
public final Buffer clear() { position = 0; limit = capacity; mark = -1; return this; }
- flip() : Buffer有兩種模式, 寫模式和讀模式. flip后Buffer從寫模式變成讀模式.
public final Buffer flip() { limit = position; position = 0; mark = -1; return this; }
(3)Buffer的實現類
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
(4)實例
public static void main(String[] args) {
//創建指定長度的緩沖區
IntBuffer buf = IntBuffer.allocate(10);
buf.put(13);
buf.put(21);
buf.put(35);
System.out.println(buf);
//從寫模式變成讀模式
buf.flip();
System.out.println(buf);
//調用get方法會使position位置向后遞增一位
for (int i = 0; i < buf.limit(); i++) {
System.out.print(buf.get() + "\t");
}
System.out.println("\n" + buf);
//清除數據
buf.clear();
System.out.println(buf);
}
java.nio.HeapIntBuffer[pos=3 lim=10 cap=10]
java.nio.HeapIntBuffer[pos=0 lim=3 cap=10]
13 21 35
java.nio.HeapIntBuffer[pos=3 lim=3 cap=10]
java.nio.HeapIntBuffer[pos=0 lim=10 cap=10]
Channel
NIO把它支持的I/O對象抽象為Channel, Channel又稱為”通道”, 類似于BIO中的流(Stream), 但有鎖區別:
- 流是單向的,通道是雙向的,可讀可寫
- 流讀寫是阻塞的,通道可以阻塞也可以非阻塞
- 流中的數據可以選擇性的先讀到緩存中,通道的數據總是要先讀寫到緩存中
(1)Channel的實現類
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
Selector
Selector選擇器充當一個監聽者, 會不斷地輪詢注冊在其上的通道(Channel)是否發生事件, 如果某個通道發生了讀寫操作, 這個通道就處于就緒狀態, 會被Selector輪詢出來, 進行后續的IO操作.
(1)Selector監聽的事件(SelectionKey)
- OP_CONNECT : 客戶端連接服務端事件
- OP_ACCEPT : 服務端接收客戶端連接事件
- OP_READ : 讀事件
- OP_WRITE : 寫事件
簡單的NIO實例
public class Server{
//選擇器(多路復用器)
private Selector selector;
//讀取的緩沖區
private ByteBuffer readBuf = ByteBuffer.allocate(1024);
//寫入的緩沖區
private ByteBuffer writeBuf = ByteBuffer.allocate(1024);
/**
* 打開選擇器,注冊服務器通道
*/
public Server(int port){
try {
//1 打開選擇器
this.selector = Selector.open();
//2 打開服務器通道
ServerSocketChannel ssc = ServerSocketChannel.open();
//3 設置服務器通道為非阻塞模式
ssc.configureBlocking(false);
//4 綁定地址
ssc.bind(new InetSocketAddress(port));
// 5 把服務器通道注冊到選擇器上,并為該通道注冊OP_ACCEPT事件.
// 當該事件到達時,selector.select()會返回,否則selector.select()會一直阻塞
ssc.register(this.selector, SelectionKey.OP_ACCEPT);
System.out.println("Server start, port :" + port);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 采用輪詢的方式監聽selector上是否有需要處理的事件,如果有,則進行處理
*/
public void listen() {
while(true){
try {
//1 當注冊的事件發生時,方法返回;否則,該方法會一直阻塞
this.selector.select();
//2 獲得selector選中項的迭代器,選中的項為注冊的事件
Iterator<SelectionKey> keys = this.selector.selectedKeys().iterator();
while(keys.hasNext()){
SelectionKey key = keys.next();
//刪除已選的key,以防重復處理
keys.remove();
//OP_ACCEPT事件發生(接收到客戶端的連接)
if(key.isAcceptable()){
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//獲得和客戶端連接的通道
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
//將SocketChannel注冊到selector上,并設置讀事件
sc.register(this.selector, SelectionKey.OP_READ);
}else if(key.isReadable()){ //OP_READ事件發生
read(key);
}else if(key.isWritable()){ //OP_WRITE事件發生
write(key);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 讀操作
*/
private void read(SelectionKey key) {
try {
//清空緩沖區舊的數據
this.readBuf.clear();
//獲取socket通道對象
SocketChannel sc = (SocketChannel) key.channel();
int count = sc.read(this.readBuf);
if(count > 0){
String msg = new String(readBuf.array());
System.out.println("服務端收到信息:"+msg);
//將SocketChannel注冊到selector上,并設置寫事件
sc.register(this.selector, SelectionKey.OP_WRITE);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 寫操作
*/
private void write(SelectionKey key){
try {
//清空緩沖區舊的數據
this.writeBuf.clear();
//獲取socket通道對象
SocketChannel sc = (SocketChannel) key.channel();
sc.write(ByteBuffer.wrap(new String("我是服務端,我已收到你的信息!").getBytes()));
//將SocketChannel注冊到selector上,并設置讀事件
sc.register(this.selector, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Server(12000).listen();
}
}
public class Client {
//選擇器(多路復用器)
private Selector selector;
//讀取的緩沖區
private ByteBuffer readBuf = ByteBuffer.allocate(1024);
//寫入的緩沖區
private ByteBuffer writeBuf = ByteBuffer.allocate(1024);
/**
* 打開選擇器,注冊客戶端通道
*/
public Client(String ip,int port){
try {
//1 打開選擇器
this.selector = Selector.open();
//2 獲得一個Socket通道
SocketChannel channel = SocketChannel.open();
//3 設置通道為非阻塞
channel.configureBlocking(false);
//4 綁定服務器ip和port
channel.connect(new InetSocketAddress(ip,port));
//5 將客戶端通道注冊到選擇器上,并為該通道注冊OP_CONNECT事件
channel.register(selector, SelectionKey.OP_CONNECT);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 采用輪詢的方式監聽selector上是否有需要處理的事件,如果有,則進行處理
*/
public void listen() {
while(true){
try {
//1 當注冊的事件發生時,方法返回;否則,該方法會一直阻塞
this.selector.select();
//2 獲得selector選中項的迭代器,選中的項為注冊的事件
Iterator<SelectionKey> keys = this.selector.selectedKeys().iterator();
while(keys.hasNext()){
SelectionKey key = keys.next();
//刪除已選的key,以防重復處理
keys.remove();
//OP_CONNECT事件發生(連接上服務器)
if(key.isConnectable()){
SocketChannel sc = (SocketChannel) key.channel();
// 如果正在連接,則完成連接
if(sc.isConnectionPending()){
sc.finishConnect();
}
sc.configureBlocking(false);
//將SocketChannel注冊到selector上,并設置寫事件
sc.register(this.selector, SelectionKey.OP_WRITE);
}else if(key.isReadable()){
read(key);
}else if(key.isWritable()){
write(key);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 讀操作
*/
private void read(SelectionKey key) {
try {
//清空緩沖區舊的數據
this.readBuf.clear();
//獲取socket通道對象
SocketChannel sc = (SocketChannel) key.channel();
int count = sc.read(this.readBuf);
if(count > 0){
String msg = new String(readBuf.array());
System.out.println("客戶端收到信息:"+msg);
//將SocketChannel注冊到selector上,并設置寫事件
sc.register(this.selector, SelectionKey.OP_WRITE);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 寫操作
*/
private void write(SelectionKey key){
try {
//清空緩沖區舊的數據
this.writeBuf.clear();
//獲取socket通道對象
SocketChannel sc = (SocketChannel) key.channel();
sc.write(ByteBuffer.wrap(new String("我是Client,我先發條信息!").getBytes()));
//將SocketChannel注冊到selector上,并設置讀事件
sc.register(this.selector, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Client("127.0.0.1",12000).listen();
}
}
結果:
客戶端先發送一條消息
服務端收到客戶端的消息,然后返回一條消息
客戶端收到服務端的消息,再發送一條消息
...
BIO和NIO的區別
(1)BIO是面向流的, 而NIO是面向緩沖的.
(2)BIO是阻塞的,而NIO是非阻塞的.
- 傳統IO方式(BIO)在調用InputStream.read()/BufferedReader.readLine()方法時是阻塞的,它會一直等到數據到來或緩沖區已滿或超時才會返回.
- NIO通過向Selector注冊讀寫事件, Selector不斷輪詢讀寫事件是否發生, 當讀寫事件發生后再去進行相應的處理.
(3)NIO的選擇器允許一個單獨的線程來監視多個輸入通道.
智能推薦
BIO/NIO/AIO編程
BIO 編程 Blocking IO: 同步阻塞的編程方式。 BIO 編程方式通常是在 JDK1.4 版本之前常用的編程方式。編程實現過程為:首先在服務端啟動一個 ServerSocket 來監聽網絡請求,客戶端啟動 Socket 發起網絡請求,默認情況下ServerSocket 回建立一個線程來處理此請求,如果服務端沒有線程可用,客戶端則會阻塞等待或遭到拒絕。 且建立好的連接,在通訊過程中,是同...
java網絡編程之NIO(二)
NIO類庫簡介 在介紹NIO編程之前,我們首先需要澄清一個概念,NIO到底是什么的簡稱?有人稱之為New IO,因為它相對于之前的IO類庫是新增的,所以被稱為New IO,這是它的官方叫法。但是,由于之前老的IO類庫是阻塞IO,New IO類庫的目標就是要讓JAVA支持非阻塞IO,所以,更多的人喜歡稱之為非阻塞IO(Non-b...
Java 網絡編程實戰筆記:BIO、NIO、AIO
Java 網絡編程學習筆記 前置概念 Java IO 模型 IO 模型 對應的 Java 版本 BIO(同步阻塞 IO) 1.4 之前 NIO(同步非阻塞 IO) 1.4 AIO(異步非阻塞 IO) 1.7 Linux 內核 IO 模型 阻塞 IO 最傳統的一種 IO 模型,在讀寫數據過程中會發生阻塞。 當用戶線程發出 IO 請求后,內核會去查看數據是否就緒,如果沒有就緒就會等待數據就緒,而用戶線...
網絡編程-BIO、NIO、AIO的原理與對比
什么是同步異步 同步和異步是針對應用程序和內核交互而言的。同步指的是用戶進程觸發IO操作并等待或者輪詢查看IO操作是否就緒。而異步就是指用戶進程觸發IO操作后便開始干自己的事情,當IO操作完成后,用戶會得到IO完成的通知。 舉個栗子: 同步:自己去銀行取錢。去了銀行申請業務,等待叫號,處理完回家 異步:委托他人代為操作,自己可以干別的,等他人取完錢交給自己。 OS操作系統底層支持異步IO操作。 什...
猜你喜歡
freemarker + ItextRender 根據模板生成PDF文件
1. 制作模板 2. 獲取模板,并將所獲取的數據加載生成html文件 2. 生成PDF文件 其中由兩個地方需要注意,都是關于獲取文件路徑的問題,由于項目部署的時候是打包成jar包形式,所以在開發過程中時直接安照傳統的獲取方法沒有一點文件,但是當打包后部署,總是出錯。于是參考網上文章,先將文件讀出來到項目的臨時目錄下,然后再按正常方式加載該臨時文件; 還有一個問題至今沒有解決,就是關于生成PDF文件...
電腦空間不夠了?教你一個小秒招快速清理 Docker 占用的磁盤空間!
Docker 很占用空間,每當我們運行容器、拉取鏡像、部署應用、構建自己的鏡像時,我們的磁盤空間會被大量占用。 如果你也被這個問題所困擾,咱們就一起看一下 Docker 是如何使用磁盤空間的,以及如何回收。 docker 占用的空間可以通過下面的命令查看: TYPE 列出了docker 使用磁盤的 4 種類型: Images:所有鏡像占用的空間,包括拉取下來的鏡像,和本地構建的。 Con...
requests實現全自動PPT模板
http://www.1ppt.com/moban/ 可以免費的下載PPT模板,當然如果要人工一個個下,還是挺麻煩的,我們可以利用requests輕松下載 訪問這個主頁,我們可以看到下面的樣式 點每一個PPT模板的圖片,我們可以進入到詳細的信息頁面,翻到下面,我們可以看到對應的下載地址 點擊這個下載的按鈕,我們便可以下載對應的PPT壓縮包 那我們就開始做吧 首先,查看網頁的源代碼,我們可以看到每一...
Linux C系統編程-線程互斥鎖(四)
互斥鎖 互斥鎖也是屬于線程之間處理同步互斥方式,有上鎖/解鎖兩種狀態。 互斥鎖函數接口 1)初始化互斥鎖 pthread_mutex_init() man 3 pthread_mutex_init (找不到的情況下首先 sudo apt-get install glibc-doc sudo apt-get install manpages-posix-dev) 動態初始化 int pthread_...