最近打算把Java網路編程相關的知識深入一下(IO、NIO、Socket編程、Netty)
Java網路編程主要涉及到對Socket和ServerSocket的使用上
閱讀之前最好有TCP和UDP協議的理論知識以及Java I/O流的基礎知識
Java I/O流
TCP協議之上構建網路程式
TCP協議的特點
-
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物件使用之后必須關閉,以釋放底層系統資源
finally 塊中關閉連接
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 with resource 語法自動關閉連接
在try塊中定義的Socket物件(以及其他實作了AutoCloseable的物件)Java會自動關閉
//在try中定義的Socket物件(或其他實作了AutoCloseable的物件)Java會自動關閉
try (Socket so = new Socket("127.0.0.1", 7001)) {
//do something
} catch(Exception e){
//
}
服務端ServerSocket
使用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關閉一個道理
Demo
下段代碼服務器在連接建立時發送一行資料到客戶端, 然后再讀取一行客戶端回傳的資料,并比較這兩行資料是否一樣,
**主執行緒只接受客戶端連接,連接建立后與客戶端的通信在一個執行緒池中完成 **
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) {
//
}
}
}
}
}
Socket選項
TCP_NODELAY
默認tcp緩沖是打開的,小資料包在發送之前會組合成更大的資料包發送, 在發送另一個包之前,本地主機需要等待對前一個包的確認-- Nagle演算法
但是這種緩沖模式有可能導致某些應用程式回應太慢(比如一個簡單的打字程式)
tcp_nodelay 設定為true關閉tcp緩沖, 所有的包一就緒就會發送
public void setTcpNoDelay(boolean on)
SO_LINGER(linger是緩慢消失、徘徊的意思)
so_linger選項指定socket關閉時如何處理尚未發送的資料報,默認是close()方法立即回傳,但是系統仍會將資料的資料發送
Linger 設定為0時,socket關閉時會丟棄所有未發送的資料
如果so_linger 打開且linger為正數,close()會阻塞指定的秒數,等待發送資料和接受確認,直到指定的秒數過去,
public void setSoLinger(boolean on, int linger)
SO_TIMEOUT
默認情況,嘗試從socket讀取資料時,read()會阻塞盡可能長的時間來獲得足夠多的位元組
so_timeout 用于設定這個阻塞的時間,當時間到期拋出一個InterruptedException例外,
public synchronized void setSoTimeout(int timeout)//毫秒,默認為0一直阻塞
SO_KEEPLIVE
so_keeplive打開后,客戶端每隔一段時間就發送一個報文到服務端已確保與服務端的連接還正常(TCP層面提供的心跳機制)
public void setKeepAlive(boolean on)
SO_RCVBUF 和SO_SNDBUF
設定tcp接受和發送緩沖區大小(內核層面的緩沖區大小)
對于傳輸大的資料塊時(HTTP、FTP),可以從大緩沖區中受益;對于互動式會話的小資料量傳輸(Telnet和很多游戲),大緩沖區沒啥幫助
緩沖區最大大小 = 帶寬 * 時延 (如果帶寬為2Mb/s, 時延為500ms, 則緩沖區最大大小為128KB左右)
如果應用程式不能充分利用帶寬,可以適當增加緩沖區大小,如果存在丟包和擁塞現象,則要減小緩沖區大小
UDP協議之上構建網路程式
UDP協議的特點
-
無連接,發送資料之前不需要建立連接,省去了建立連接的開銷
-
盡力最大努力交付,資料報可能丟失、亂序到達
-
面向報文(UDP對應用層交下來的報文,既不合并,也不拆分,而是保留這些報文的邊界)
-
UDP沒有擁塞控制
-
UDP支持一對一、一對多、多對一和多對多的互動通信
-
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客戶端
因為UDP是無連接的,所以構造DatagramSocket的時候只需要指定本地埠, 不需要指定遠程主機和埠
遠程主機的主機和埠是指定在資料報中的,所以UDP可以實作一對一、一對多、多對多傳輸
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服務端
UDP服務端同客戶端一樣使用的是DatagramSocket, 區別在于綁帶的本地埠需要顯示申明
下面的UDP服務端程式接受客戶端的報文,從報文中獲取請求主機和埠,然后回傳固定的資料內容 "received"
byte[] data = "https://www.cnblogs.com/pepper-0611/p/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
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/155040.html
標籤:Java
上一篇:Java 網路編程
