TCP 網絡編程
一、 JAVA-IO分類:
BIO,NIO,AIO (NIO 2.0)。
BIO 同步阻塞IO (blocking I/O): 服務器處理客戶端的連接請求業務需要開啓一個線程來進行業務,這樣連接的資源越多服務器承載的消耗會變大。
NIO 同步非阻塞 (non-blocking I/O): 同步非阻塞,客戶端發送的連接請求都會註冊到多路複用器上,多路複用器輪詢到連接有I/O請求時會得到需要處理的客戶端對其進行處理。這種方式就大大減少了過多線程資源的開銷。
AIO 異步非阻塞 (Asynchronous I/O) : 異步非阻塞,客戶端連接或請求時,服務器的某些狀態改變後通知對應處理方法。例如客戶端連接服務器時,服務器知道客戶端連接則通知處理accept方法,客戶端發送給服務器數據,服務器知道有某客戶端的數據則通知處理read方法。
NIO 實際上使用一個線程開始輪訓選擇有狀態的客戶端,發現某客戶端有狀態變化後挨個處理,這種處理方式業務實現上一般不要有過長的業務堵塞(例如 較長IO操作)。
AIO 實際上是有狀態的客戶端,調用一個線程來處理這部分業務。處理完之後如果還需要繼續監聽這個狀態,則還需要重新註冊。
以下代碼可以copy到編輯器中調試,使用。
二、 BIO JAVA 實現
這裏我實現一個簡單的發送字符串的例子,服務器接受客戶端發送的字符串,每一段完整的內容直接都使用\n進行分割。
客戶端發送兩條消息: 1234567890\n34567899\n
因爲消息發送出來的時候,會出現兩種情況:
1. 兩個數據包粘在一起需要拆包。
2. 一個數據包第一次獲取並不完整,分爲多次獲取才取完。
1. 服務器實現
BioServer.class
public class BioServer {
public static void main(String[] args) throws Exception {
// 創建一個服務器, 端口號 11111, 最大客戶端連接: 10000
ServerSocket server = new ServerSocket(11111, 10000);
while (true) {
// 等待接受客戶端的連接,如果有客戶端連接則會返回Socket對象,沒有則阻塞。
Socket client = server.accept();
// 這裏需要開啓一個線程來保存與這個客戶端的連接,然後繼續接受下一個客戶端。
Thread thread = new Thread(new BioServerReaderHandler(client) ) ;
thread.start(); // 啓動線程
}
}
}
BioServerReaderHandler.class
public class BioServerReaderHandler implements Runnable {
private Socket socket;
public BioServerReaderHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
byte[] buff = new byte[1024];
ByteArrayOutputStream cache = new ByteArrayOutputStream(1024);
while (!socket.isClosed() && !socket.isInputShutdown()) {
try {
int len = in.read(buff);
// 讀到 \n 則輸出
for (int i = 0; i < len; i++) {
byte b = buff[i];
// 通過 \n 用來分割每個數據包, 能夠完整拆除一個包則直接輸出
if (b == '\n') {
byte[] lineBytes = cache.toByteArray();
String text = new String(lineBytes, Charset.forName("UTF-8"));
// 客戶端關閉客戶端
if (text.trim().equals("quit")) return;
// 輸出內容
System.out.println(text);
// 返回應答
out.write("已經收到一條消息\n".getBytes(Charset.forName("UTF-8")));
// 清空緩存
cache = new ByteArrayOutputStream(1024);
} else {
// 不滿足條件則放入緩存
cache.write(b);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
} catch (IOException e1) {
e1.printStackTrace();
} finally {
try {
socket.close();
} catch (Exception e) {
}
}
}
}
2. 客戶端實現
這裏寫一個類似 telnet 的客戶端, 通過控制檯輸入獲取一行內容發送給服務器,服務器回傳的信息顯示到控制檯上。
發送服務器的消息和接受服務器回覆的消息都是 \n 分割。
如下就實現了一個簡單的客戶端,控制檯的消息發送給服務器,服務器回覆的消息再顯示到控制檯。
BioClient.class
public class BioClient {
public static void main(String[] args) throws Exception {
// 這裏寫一個類似 telnet 的客戶端
Socket client = new Socket("127.0.0.1", 11111);
// 這裏開啓一個線程異步讀取服務器返回的消息
Thread thread = new Thread(new BioClientReaderHandler(client));
thread.start();
// 獲得向服務器輸出的流對象
OutputStream out = client.getOutputStream();
Scanner in = new Scanner(System.in);
// 監聽控制檯寫入的內容,發送給服務器
while (!client.isClosed() && !client.isOutputShutdown()) {
// 從控制檯中讀取一行數據 加上 \n 分割符 發送給服務器
String line = in.nextLine();
byte[] lineBytes = (line + "\n").getBytes(Charset.forName("UTF-8"));
out.write(lineBytes);
// 退出
if (line.trim().equals("quit")) {
break;
}
}
client.close();
}
}
BioClientReaderHandler.class
public class BioClientReaderHandler implements Runnable {
private Socket socket;
public BioClientReaderHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
InputStream in = socket.getInputStream();
byte[] buff = new byte[1024];
ByteArrayOutputStream cache = new ByteArrayOutputStream(1024);
while (!socket.isClosed() && !socket.isInputShutdown()) {
try {
int len = in.read(buff);
// 讀到 \n 則輸出
for (int i = 0; i < len; i++) {
// 通過 \n 用來分割每個數據包, 能夠完整拆除一個包則直接輸出
if (buff[i] == '\n') {
byte[] lineBytes = cache.toByteArray();
String text = new String(lineBytes, Charset.forName("UTF-8"));
// 輸出內容
System.out.println(text);
cache = new ByteArrayOutputStream(1024);
} else {
// 不滿足條件則放入緩存
cache.write(buff[i]);
}
}
} catch (Exception e) {
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
測試,啓動BioServer,然後啓動BioClient 連接服務器或 直接使用 telnet 連接服務器 。
BioClient:
BioServer
三、NIO JAVA 實現。
這裏也同樣實現一個字符串消息傳輸,但是此時我會直接在最開始的時候告知可以讀取的內容長度是多少。
這裏我定義一個消息結構:
message {
int length; // int 佔 4byte, 下面我這麼代表 [][][][]
String msg; // 不固定長度的內容,長度通過length得到。
}
這樣我發送每個消息的時候在消息頭開始攜帶這個消息的長度,按照固定長度分割每個消息包。
一連串消息格式 : [][][][]內容......[][][][]內容......[][][][]內容......[][][][]內容......
[][][][] 代表4子節
這裏會用到這些功能:
Selector 多路選擇器
SocketChannel 客戶端的sokcet通道
ServerSocketChannel 服務器端的socket通道
ByteBuffer 1. 讀取或回覆socket數據,2. 獲取的數據包不完整時、先加入緩存。
點擊可以查看: ByteBuffer使用
NIO 服務器端
NioServer.class .
功能: 收到客戶端發來的消息包,拆包結構後輸出內容, 並回復客戶端。 接受 quit 關閉客戶端
個人感覺Nio的ByteBuffer 理解上不太好用。
Selector 你可以理解爲一個集合,將需要監聽狀態的對象放裏面,通過循環一個一個判斷狀態,滿足狀態的取出。
具體看代碼, 每行代碼都給一定解釋
public class NioServer {
public static void main(String[] args) throws Exception {
// 多路複用選擇
Selector selector = Selector.open();
// 開啓服務器Socket通道
ServerSocketChannel server = ServerSocketChannel.open();
// 監聽 11111 端口、設置允許 10000 連接等待
server.bind(new InetSocketAddress(11111), 10000);
// 非阻塞、所以這裏要配置一下,false爲非阻塞
server.configureBlocking(false);
// 註冊選擇器,OP_ACCEPT 的狀態,如果有這個則會被輪訓選擇出來
server.register(selector, SelectionKey.OP_ACCEPT);
while (server.isOpen()) {
// 設置空輪訓的間隔
selector.select(3000);
// selectedKeys 選擇有狀態的需要進行操作的項
Iterator<SelectionKey> selectKeys = selector.selectedKeys().iterator();
while (selectKeys.hasNext()) {
// 獲取等待操作的項
SelectionKey selectionKey = selectKeys.next();
// 操作完成消費掉當前項。
selectKeys.remove();
if (selectionKey.isValid()) {
// 有客戶端的連接需要被接受
if (selectionKey.isAcceptable()) {
ServerSocketChannel ser = (ServerSocketChannel)selectionKey.channel();
// 接收到客戶端了
SocketChannel client = ser.accept();
// 將客戶端設置爲非阻塞
client.configureBlocking(false);
// 註冊選擇器, OP_READ 的狀態如果存在時候會被輪訓選擇出來,ByteBuffer.allocate(10240) 用來緩存不完整的消息包
client.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(10240));
}
// 有未讀完的數據
if (selectionKey.isReadable()) {
SocketChannel client = (SocketChannel)selectionKey.channel();
// 其實到這一步就完成了簡單的NIO服務器,read 用來讀,write用來回復。
// client.read(dst)
// client.write(src)
// 下面演示用 ByteBuffer 和 SocketChannel 合用的拆包過程
// ByteBuffer 寫的數據需要調用 flip() 轉化讀的狀態,
// 他是將 limit賦值position,position=0,可讀長度就爲remaining() = limit - position。
// []代表每個byte,[-]代表有值。
// 寫的時候 position 記錄寫的位置。limit 代表最大可寫位置
// [-][-][-][-][-][-][position][][][][][][][]limit limit - position 是剩餘可寫,寫的時候position會向右++
// 讀的時候 position 記錄讀的位置。limit 代表最大可讀位置
// [position][-][-][-][-][-][limit][][][][][][][]capacity flip()後 limit - position 是剩餘可讀 讀的時候position會向右++
// 開始讀取客戶端的操作,讀取內容後輸出。
// attachment 是一個可以在當前選擇Key中傳遞的參數,方便存儲一些必要操作的內容。
ByteBuffer cache = (ByteBuffer)selectionKey.attachment();
// 用一個臨時變量緩存取接受讀的數據。
ByteBuffer readBuff = ByteBuffer.allocate(1024);
int len = client.read(readBuff); // 將讀的數據寫入 ByteBuffer
if (len == -1) {
selectionKey.cancel();
client.close();
continue;
}
readBuff.flip(); // 轉化爲讀
// 放不下了, 增加一點容量(讀取到的長度大於cache可寫空間)
if (len > cache.remaining()) {
cache = ByteBuffer.allocate(cache.capacity() + len).put(cache);
selectionKey.attach(cache);
}
// 將讀讀到的數據先放入緩存,在後面在做拆包處理。
cache.put(readBuff); // 寫
cache.flip(); // 轉換爲讀 此時 limit = position, position = 0
// 獲得可讀空間大於 4 字節, 因爲定義的數據結構是 前4字節記錄的body長度
if (cache.remaining() < 4) {
// 還將position,limit 位置 原爲寫時的狀態
cache.position(cache.limit());
cache.limit(cache.capacity());
continue;
}
int length = cache.getInt(); // 獲取內容的總長度
if (length < cache.remaining()) { // 判斷當前內容可讀長度 不滿足 數據內容長度,則繼續還原
// 還將position,limit 位置 原爲寫時的狀態
cache.position(cache.limit());
cache.limit(cache.capacity());
continue;
}
// 讀取成功
byte[] body = new byte[length];
cache.get(body);
String text = new String(body, Charset.forName("UTF-8"));
cache.compact(); // 已讀部分移除、又轉化爲可寫的情況。
if (text.equals("quit")) {
selectionKey.cancel(); // 從多路選擇上移除自己
client.close(); // 關閉客戶端
System.out.println("再見!"); // 再見!
continue;
}
System.out.println("客戶端發來的內容: " + text);
// 認真的回覆客戶端 我已經成功接收到!
byte[] reply = "我已經成功接收到!".getBytes(Charset.forName("UTF-8"));
ByteBuffer replyBuff = ByteBuffer.allocate(4 + reply.length).putInt(reply.length).put(reply);
replyBuff.flip();
client.write(replyBuff);
}
}
}
}
}
}
NIO 客戶端:
客戶端,從控制的輸入內容,發送給服務器。並異步接受服務器的回覆。發送 quit 關閉客戶端
NioClient.class
public class NioClient {
public static void main(String[] args) throws Exception {
// 多路選擇器 (這個如果你開了好幾個客戶端,都可以註冊到同一個Selector)
Selector selector = Selector.open();
// 客戶端開啓了
SocketChannel client = SocketChannel.open();
// 連接到你想要連接的
client.connect(new InetSocketAddress("127.0.0.1", 11111));
// 跟服務器一樣 需要設置爲非阻塞
client.configureBlocking(false);
// 註冊到 Selector, 輪訓時 OP_READ 狀態則會被選中
SelectionKey selectionKey = client.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(10240));
// 開始輪訓,這裏我們使用一個線程進行輪訓。
Thread thread = new Thread(new NioSelectorLoopHandler(selector));
thread.start();
// 下面從控制檯輸入內容發送到服務器中
Scanner in = new Scanner(System.in);
while (true) {
String line = in.nextLine();
byte[] body = line.getBytes(Charset.forName("UTF-8"));
int length = body.length;
ByteBuffer byteBuffer = ByteBuffer.allocate(4 + length);
byteBuffer.putInt(length);
byteBuffer.put(body);
byteBuffer.flip();
// 發送信息給服務器
client.write(byteBuffer);
// 關閉了這個客戶端
if (line.equals("quit")) {
selectionKey.cancel();
client.close();
break;
}
}
selector.close();
}
}
NioSelectorLoopHandler.class 多路選擇器輪訓,開啓的一個線程進行輪訓。
public class NioSelectorLoopHandler implements Runnable {
private Selector selector;
public NioSelectorLoopHandler(Selector selector) {
this.selector = selector;
}
@Override
public void run() {
while (selector.isOpen()) {
try {
selector.select(3000);
Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
while (selectionKeys.hasNext()) {
SelectionKey selectionKey = selectionKeys.next();
selectionKeys.remove();
if (selectionKey.isValid()) {
if (selectionKey.isReadable()) {
SocketChannel client = (SocketChannel)selectionKey.channel();
ByteBuffer cache = (ByteBuffer)selectionKey.attachment();
// 用一個臨時變量緩存取接受讀的數據。
ByteBuffer readBuff = ByteBuffer.allocate(1024);
int len = client.read(readBuff); // 將讀的數據寫入 ByteBuffer
readBuff.flip(); // 轉化爲讀
// 放不下了, 增加一點容量(讀取到的長度大於cache可寫空間)
if (len > cache.remaining()) {
cache = ByteBuffer.allocate(cache.capacity() + len).put(cache);
selectionKey.attach(cache);
}
// 將讀讀到的數據先放入緩存,在後面在做拆包處理。
cache.put(readBuff); // 寫
cache.flip(); // 轉換爲讀 此時 limit = position, position = 0
// 獲得可讀空間大於 4 字節, 因爲定義的數據結構是 前4字節記錄的body長度
if (cache.remaining() < 4) {
// 還將position,limit 位置 原爲寫時的狀態
cache.position(cache.limit());
cache.limit(cache.capacity());
continue;
}
int length = cache.getInt(); // 獲取內容的總長度
if (length < cache.remaining()) { // 判斷當前內容可讀長度 不滿足 數據內容長度,則繼續還原
// 還將position,limit 位置 原爲寫時的狀態
cache.position(cache.limit());
cache.limit(cache.capacity());
continue;
}
// 讀取成功
byte[] body = new byte[length];
cache.get(body);
String text = new String(body, Charset.forName("UTF-8"));
cache.compact(); // 已讀部分移除、又轉化爲可寫的情況。
// 輸出
System.out.println(text);
}
}
}
} catch (Exception e) {
//e.printStackTrace();
}
}
}
}
現在啓動 NioServer, 在啓動NioClient 連接服務器,
測試結果:
NioClient :
NioServer:
四、AIO (NIO 2.0) 非阻塞異步IO
這裏實現異步IO的服務端,消息的結構依舊是 \n 分割即可。這樣可以直接使用telnet做客戶端測試。
消息: 好多內容....\n又是好多內容.....\n
這裏需要用到:
AsynchronousChannelGroup 異步通知處理的的線程池。
AsynchronousServerSocketChannel 異步IO服務Socket通道。
ByteBuffer 1. 接受消息或寫消息,2. 緩存未處理的消息。
異步iO的使用,需要一次執行一次註冊。例如accept 狀態會通知指定的處理方法,處理完需要再次重新註冊。
AIO 服務器
AioServer.class 可以直接複製去直接調試.
public class AioServer {
public static void main(String[] args) throws Exception {
// 創建了一個線程池來執行 後續的處理器
ExecutorService executor = Executors.newFixedThreadPool(100);
// 第一個參數線程池,第二個參數,啓動時直接開啓這麼多個線程
AsynchronousChannelGroup group = AsynchronousChannelGroup.withCachedThreadPool(executor, 10);
// 創建服務
AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(group);
// 綁定 11111, 可以允許接受 10000待處理的客戶端。
server.bind(new InetSocketAddress(11111), 10000);
// 第一個參數是允許傳遞全局的對象。第二個參數是註冊 accept,有這個狀態是通過哪個處理器執行。
server.accept(null, new AioAcceptHandler(server));
}
}
AioAcceptHandler.class 用於異步接受客戶端連接
/**
* 自己實現一個處理器
*
*/
public class AioAcceptHandler implements CompletionHandler<AsynchronousSocketChannel, Object> {
AsynchronousServerSocketChannel server;
public AioAcceptHandler(AsynchronousServerSocketChannel server) {
this.server = server;
}
@Override
public void completed(AsynchronousSocketChannel client, Object attachment) {
// attachment 是上次註冊傳遞過來的對象。
// 當已經觸發accept後,需要重新註冊自己, 這樣可以繼續接受新的客戶端連接
server.accept(attachment, this);
// 下面要註冊服務器接受客戶端的消息處理器
ByteBuffer msg = ByteBuffer.allocate(1024); // 讀取的數據會填充這個對象
ByteBuffer cache = ByteBuffer.allocate(10240); // 這個對象用於緩存未完成處理的數據
// 這裏註冊自己實現的讀取處理器, 有讀的內容就會進去處理了
client.read(msg, cache, new AioReaderHandler(client, msg));
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.fillInStackTrace();
}
AioReaderHandler.class 用於異步接受處理客戶端的消息
/**
* 自己實現一個客戶端讀取處理器
*/
public class AioReaderHandler implements CompletionHandler<Integer, ByteBuffer>{
private AsynchronousSocketChannel client;
private ByteBuffer msg; // 讀的數據會填充這個
public AioReaderHandler(AsynchronousSocketChannel client, ByteBuffer msg) {
this.client = client;
this.msg = msg; // 讀的數據會填充這個
}
@Override
public void completed(Integer result, ByteBuffer cache) {
// 其實這裏就已經OK了
// msg.flip(), msg.get(byte[]) 讀取數據
// client.write(ByteBuffer) 回覆
// client.read(msg, cache, this); 重新註冊 第一個參數是下次要讀的內容,第二個參數是全局攜帶的對象
// 下面是 對消息包進行拆包的示例,假設每個消息包都使用\n分割。
ByteBuffer msg = this.msg;
try {
if (result == -1) {
client.close();
return; // 關閉了不再讀
}
// 1 獲得客戶端發來的數據
// 轉讀狀態,msg.remaining() 就是可讀內容了長度
msg.flip();
byte[] data = new byte[msg.remaining()];
msg.get(data); // 讀完了。
msg.clear(); // 清空 msg
// 2 判斷是否有 \n 分割符,如果存在就把緩存裏的值和當前內容輸出。
// 客戶端接收到的消息集合
List<String> results= new ArrayList<String>();
// 讀取每個\n分割的內容,分割成功的內容放入 results集合內
for (int i = 0; i < data.length; i++) {
if (data[i] == '\n') {
cache.flip(); // 讀
// 讀所有
byte[] body = new byte[cache.remaining()];
cache.get(body);
// 得到最終讀內容 將內容加入 結果集合,在後面進行處理結果
String text = new String(body, Charset.forName("UTF-8"));
results.add(text); // 拆開一個完整的消息添加到集合
// 已經全部輸出了,清空緩存
cache.clear();
} else {
// 空間不足就擴大空間繼續緩存
if (cache.remaining() == 0) {
cache = ByteBuffer.allocate(cache.capacity() + 1024).put(cache);
}
// 不滿足條件加入緩存
cache.put(data[i]);
}
}
// 3 結果處理,處理接受到的每一段消息。
// results 是當前解析的消息包集合 每個消息\n分割解析後的結果
// 輸出讀取到的內容,每次輸出都回復給客戶端 "成功收到消息! \n"
for (String text : results) {
if (text.trim().equals("quit")) {
System.out.println("再見!");
client.close();
return; // 關閉了不再讀
}
System.out.println("去讀到客戶端消息: " + text);
byte[] reply = "成功收到消息!\n".getBytes(Charset.forName("UTF-8"));
ByteBuffer replyBuffer = ByteBuffer.allocate(reply.length).put(reply);
replyBuffer.flip();
client.write(replyBuffer);
}
// 執行完讀取後,繼續註冊,進行下一次讀取。
client.read(msg, cache, this);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
測試結果:
客戶端:
服務端: