最近打算把Java網絡編程相關的知識深刻一下(IO、NIO、Socket編程、Netty)html
Java網絡編程主要涉及到對Socket和ServerSocket的使用上java
閱讀以前最好有TCP和UDP協議的理論知識以及Java I/O流的基礎知識node
TCP是面向鏈接的協議,通訊以前須要先創建鏈接編程
提供可靠傳輸,經過TCP傳輸的數據無差錯、不丟失、不重複、而且按序到達數組
面向字節流(雖然應用程序和TCP的交互是一次一個數據塊,可是TCP把應用程序交下來的數據僅僅當作是一連串的無結構的字節流)服務器
點對點全雙工通訊網絡
擁塞控制 & 滑動窗口異步
咱們使用Java構建基於TCP的網絡程序時主要關心客戶端Socket和服務端ServerSocket兩個類socket
使用客戶端SOCKET的生命週期:鏈接遠程服務器 --> 發送數據、接受數據... --> 關閉鏈接
構造函數裏指定遠程主機和端口, 構造函數正常返回即表明鏈接成功, 鏈接失敗會拋IOException或者UnkonwnHostException
public Socket(String host, int port) public Socket(String host, int port, InetAddress localAddr,int localPort)
當使用無參構造函數時,通訊前須要手動調用connect進行鏈接(同時可設置SOCKET選項)
Socket so = new Socket(); SocketAddress address = new InetSocketAddress("www.baidu.com", 80); so.connect(address);
Java的I/O創建於流之上,讀數據用輸入流,寫數據用輸出流
下段代碼鏈接本地7001端口的服務端程序,讀取一行數據而且將該行數據回寫服務端。
try (Socket so = new Socket("127.0.0.1", 7001)) { BufferedReader reader = new BufferedReader(new InputStreamReader(so.getInputStream())); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(so.getOutputStream())); //read message from server String recvMsg = reader.readLine(); //write back to sever. writer.write(recvMsg); writer.newLine(); writer.flush(); } catch (IOException e) { //ignore }
大端模式是指數據的高字節保存在內存的低地址中(默認或者說咱們閱讀習慣都是大端模式)
Socket對象使用以後必須關閉,以釋放底層系統資源
Socket so = null; try { so = new Socket("127.0.0.1", 7001); // }catch (Exception e){ // }finally { if(so != null){ try { so.close(); } catch (IOException e) { // } } }
在try塊中定義的Socket對象(以及其餘實現了AutoCloseable的對象)Java會自動關閉
//在try中定義的Socket對象(或其餘實現了AutoCloseable的對象)Java會自動關閉 try (Socket so = new Socket("127.0.0.1", 7001)) { //do something } catch(Exception e){ // }
使用ServerSocket的生命週期:綁定本地端口(服務啓動) --> 監聽客戶端鏈接 --> 接受客戶端鏈接 --> 經過該客戶端鏈接與客戶端進行通訊 --> 監聽客戶端鏈接 --> .....(loop) --> 關閉服務器
直接在構造函數中指定端口完成綁定或者手工綁定
//構造函數中指定端口完成綁定 ServerSokect ss = new ServerSocket(7001); //手工調用bind函數完成綁定 ServerSokect ss = new ServerSocket(); ss.bind(new InetSocketAddress(7001));
accept方法返回一個Socket對象,表明與客戶端創建的一個鏈接
ServerSokect ss = new ServerSocket(7001); while(true){ //阻塞等待鏈接創建 Socket so = ss.accept(); // do something. }
經過鏈接創建後的Socket對象,打開輸入流、輸出流便可與客戶端進行通訊
同客戶端Socket關閉一個道理
下段代碼服務器在鏈接創建時發送一行數據到客戶端, 而後再讀取一行客戶端返回的數據,並比較這兩行數據是否同樣。
**主線程只接受客戶端鏈接,鏈接創建後與客戶端的通訊在一個線程池中完成 **
public class BaseServer { private static final String MESSAGE = "hello, i am server"; private static ExecutorService threads = Executors.newFixedThreadPool(6); public static void main(String[] args) { //try with resource 寫法綁定本地端口 try (ServerSocket socket = new ServerSocket(7001)) { while (true) { //接受客戶端鏈接 Socket so = socket.accept(); //與客戶端通訊的工做放到線程池中異步執行 threads.submit(() -> handle(so)); } } catch (IOException e) { // } } public static void handle(Socket so) { //try with resource 寫法打開輸入輸出流 try (InputStream in = so.getInputStream(); OutputStream out = so.getOutputStream()) { BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, "utf-8")); BufferedReader reader = new BufferedReader(new InputStreamReader(in)); //send data to client. writer.write(MESSAGE); writer.newLine(); writer.flush(); //recv data from client. String clientResp = reader.readLine(); System.out.println(MESSAGE.equals(clientResp)); } catch (Exception e) { //ignore }finally { //關閉socket if(so != null){ try { so.close(); } catch (IOException e) { // } } } } }
默認tcp緩衝是打開的,小數據包在發送以前會組合成更大的數據包發送, 在發送另外一個包以前,本地主機須要等待對前一個包的確認-- Nagle算法
可是這種緩衝模式有可能致使某些應用程序響應太慢(好比一個簡單的打字程序)
tcp_nodelay 設置爲true關閉tcp緩衝, 全部的包一就緒就會發送
public void setTcpNoDelay(boolean on)
so_linger選項指定socket關閉時如何處理還沒有發送的數據報,默認是close()方法當即返回,可是系統仍會將數據的數據發送
Linger 設置爲0時,socket關閉時會丟棄全部未發送的數據
若是so_linger 打開且linger爲正數,close()會阻塞指定的秒數,等待發送數據和接受確認,直到指定的秒數過去。
public void setSoLinger(boolean on, int linger)
默認狀況,嘗試從socket讀取數據時,read()會阻塞儘量長的時間來得到足夠多的字節
so_timeout 用於設置這個阻塞的時間,當時間到期拋出一個InterruptedException異常。
public synchronized void setSoTimeout(int timeout)//毫秒,默認爲0一直阻塞
so_keeplive打開後,客戶端每隔一段時間就發送一個報文到服務端已確保與服務端的鏈接還正常(TCP層面提供的心跳機制)
public void setKeepAlive(boolean on)
設置tcp接受和發送緩衝區大小(內核層面的緩衝區大小)
對於傳輸大的數據塊時(HTTP、FTP),能夠從大緩衝區中受益;對於交互式會話的小數據量傳輸(Telnet和不少遊戲),大緩衝區沒啥幫助
緩衝區最大大小 = 帶寬 * 時延 (若是帶寬爲2Mb/s, 時延爲500ms, 則緩衝區最大大小爲128KB左右)
若是應用程序不能充分利用帶寬,能夠適當增長緩衝區大小,若是存在丟包和擁塞現象,則要減少緩衝區大小
無鏈接。發送數據以前不須要創建鏈接,省去了創建鏈接的開銷
盡力最大努力交付。數據報可能丟失、亂序到達
面向報文(UDP對應用層交下來的報文,既不合並,也不拆分,而是保留這些報文的邊界)
UDP沒有擁塞控制
UDP支持一對1、一對多、多對一和多對多的交互通訊
UDP的首部開銷小,只有8個字節,比TCP的20個字節的首部還要短。
構建UDP協議的網絡程序時, 咱們關係DatagramSocket和DatagramPacket兩個類
UDP是面向報文傳輸的,對應用層交下來的報文不合並也不拆分(TCP就存在拆包和粘包的問題)
數據報關心兩個事:存儲報文的底層字節數組 和 通訊對端地址(對端主機和端口)
//發送數據報指定發送的數據和對端地址 DatagramPacket sendPacket = new DatagramPacket(new byte[0], 0, InetAddress.getByName("127.0.0.1"), 7002); //接受數據報只須要指定底層字節數組以及其大小 DatagramPacket recvPacket = new DatagramPacket(new byte[1024], 1024);
由於UDP是無鏈接的,因此構造DatagramSocket的時候只須要指定本地端口, 不須要指定遠程主機和端口
遠程主機的主機和端口是指定在數據報中的,因此UDP能夠實現一對1、一對多、多對多傳輸
try (DatagramSocket so = new DatagramSocket(0)) { //數據報中指定對端地址(服務端地址) DatagramPacket sendPacket = new DatagramPacket(new byte[0], 0, InetAddress.getByName("127.0.0.1"), 7002); //發送數據報 so.send(sendPacket); //阻塞接受數據報 DatagramPacket recvPacket = new DatagramPacket(new byte[1024], 1024); so.receive(recvPacket); //打印對端返回的數據 System.out.println(new String(recvPacket.getData(), 0, recvPacket.getLength())); } catch (Exception e) { e.printStackTrace(); }
UDP服務端同客戶端同樣使用的是DatagramSocket, 區別在於綁帶的本地端口須要顯示申明
下面的UDP服務端程序接受客戶端的報文,從報文中獲取請求主機和端口,而後返回固定的數據內容 "received"
byte[] data = "received".getBytes(); try (DatagramSocket so = new DatagramSocket(7002)) { while (true) { try { DatagramPacket recvPacket = new DatagramPacket(new byte[1024], 1024); so.receive(recvPacket); DatagramPacket sendPacket = new DatagramPacket(data, data.length, recvPacket.getAddress(), recvPacket.getPort()); so.send(sendPacket); } catch (Exception e) { // } } } catch (SocketException e) { // }
UDP是無鏈接的, 可是DatagramSocket提供了鏈接功能對通訊對端進行限制(並非真的鏈接)
鏈接以後只能向指定的主機和端口發送數據報, 不然會拋出異常。
鏈接以後只能接收到指定主機和端口發送的數據報, 其餘數據報會被直接拋棄。
public void connect(InetAddress address, int port) public void disconnect()
Java 中TCP編程依賴於 Socket和ServerSocket,UDP編程依賴於DatagramSocket和DatagramPacket