背景:
省分短信發送每天都差不多要1000W條上下,遇到特殊節假日和政府通告時量會更大!boss系統中存放的是短信發送內容,而真正完成發送短信指令動作是的華爲方做的短廳,這麼大的通信量選擇了netty來完成數據傳輸並自定義了一套基於netty的SGIP協議進行通信;
省分boss系統—>短信營業廳();
基本知識
TCP/IP網絡協議:
網上很多有關這個協議的解釋,自行google,下面是簡單的理解記憶:
tcp/ip的3次握手, 簡單來說就是第一次我連接你給你一個標識SYN,你給我返回SYN並給一個新的ACK標記我,然後我再把ACK給你,
這樣證明我們之前傳東西是可靠,的然後就正式傳數據了
圖片來自網上
tcp/ip的4次揮手斷開,相當於,你給我一個ACK我給你一個FIN,然後再次彼此交換確認,OK就可以結束通信了
java的socket就是對tcp/ip的一種實現
基礎代碼,:
一個簡單的socket實現tcp/ip的樣例,後面的BIO/NIO/AIO都是基本上於這個例子進行變化
client端:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class Client {
final static String ADDRESS = "127.0.0.1";
final static int PORT = 7788;
public static void main(String[] args)
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
socket = new Socket(ADDRESS, PORT);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
//向服務器端發送數據
out.println("接收到客戶端的請求數據...");
out.println("接收到客戶端的請求數據1111...");
String response = in.readLine();
System.out.println("Client: " + response);
...
Server端:
public class Server {
final static int PROT = 7788;
public static void main(String[] args) {
ServerSocket server = null;
server = new ServerSocket(PROT);
System.out.println(" server start .. ");
//進行阻塞
Socket socket = server.accept();
//新建一個線程執行客戶端的任務
new Thread(new ServerHandler(socket)).start();
}
}
ServerHandler.java 如下:
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 body = null;
while(true){
body = in.readLine();
if(body == null) break;
System.out.println("Server :" + body);
out.println("服務器端回送響的應數據.");
}
}
}
上面這個代碼很簡單轉換成圖型說明就是web瀏覽器發一個請求過來,web服務器就要new 一個線程來處理這個請求,這是傳統的請求處理模型,這也就引來一個很大的問題,當請求越多,服務器端的啓用線程也要越多,我們都知道linux(window)的文件句柄數有是限的,默認是1024,當然可以修改,上限好像是65536 ,(一個柄也相當於一個socket也相當於一個thread,linux查看文件句柄Unlimit -a) 其實在實際當中只要併發到1000上下響應請求就會很慢了,所以這種模型是有問題的,這種也就是同步阻塞IO編程(JAVA BIO)
網上查的定義:
同步阻塞IO(JAVA BIO):
同步並阻塞,服務器實現模式爲一個連接一個線程,即客戶端有連接請求時服務器端就需要啓動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷.
BIO—>NIO—->AIO的發展歷程
上面的BIO是有問題的,也就是出現在jdk1.4那個古時代的產物,現在當然這要改進下,上面的問題無非就是服務器端的線程無限制的增長才會導致服務器崩掉,那我們就對徵下藥,加個線程池限制線程的生成,又可以複用空閒的線程,是的,在jdk1.5也是這樣做的,下面是服務器端改進後的代碼:
public class Server {
final static int PORT = 7788;
public static void main(String[] args) {
ServerSocket server = null;
BufferedReader in = null;
PrintWriter out = null;
server = new ServerSocket(PORT);
System.out.println("server start");
Socket socket = null;
HandlerExecutorPool executorPool = new HandlerExecutorPool(50, 1000);
while(true){
socket = server.accept();
executorPool.execute(new ServerHandler(socket));
}
}
}
HandlerExecutorPool.java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class HandlerExecutorPool {
private ExecutorService executor;
public HandlerExecutorPool(int maxPoolSize, int queueSize){
this.executor = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(),
maxPoolSize,
120L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(queueSize));
}
public void execute(Runnable task){
this.executor.execute(task);
}
}
Jdk1.5創造了一個假的nio 用一個HanderExecutorPool來限定了線程數量,但只是解決了服務器端不會因爲併發太多而死掉,但解決不了併發大而響應越來越慢的,到時你也會懷疑你是不是真的用了一個假的nio!!!!!!!
爲了解決這個問題,就要用三板斧來解決!
別急,要解決一個諸葛亮,你必先要造三個臭皮匠,先引入3個NIO相關概念先!
1> Buffer 緩衝區
難用的buffer是一個抽象的對象,下面還有ByteBuffer,IntBuffer,LongBuffer等子類,相比老的IO將數據直接讀/寫到Stream對象,NIO是將所有數據都用到緩衝區處理,它本質上是一個數組,提供了位置,容量,上限等操作方法,還是直接看代碼代碼來得直接
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 7788);//創建連接的地址
SocketChannel sc = null;//聲明連接通道
ByteBuffer buf = ByteBuffer.allocate(1024);//建立緩衝區
sc = SocketChannel.open();//打開通道
sc.connect(address);//進行連接
while(true){
//定義一個字節數組,然後使用系統錄入功能:
byte[] bytes = new byte[1024];
System.in.read(bytes);
buf.put(bytes);//把數據放到緩衝區中
buf.flip();//對緩衝區進行復位
sc.write(buf);//寫出數據
buf.clear();//清空緩衝區數據
}
2>Channel 通道
如自來水管一樣,支持網絡數據從Channel中讀寫,通道寫流最大不同是通道是雙向的,而流是一個方向上移動(InputStream/OutputStream),通道可用於讀/寫或讀寫同時進行,它還可以和下面要講的selector結合起來,有多種狀態位,方便selector去識別. 通道分兩類,一:網絡讀寫(selectableChannel),另一類是文件操作(FileChannel),我們常用的是上面例子中的網絡讀寫!
3>Selector 多路複用選擇器
它是神一樣存在的東西,多路複用選擇器提供選擇已經就緒的任務的能力,也就是selector會不斷輪詢註冊在其上的通道(Channel),如果某個通道發生了讀寫操作,這個通道處於就緒狀態,會被selector輪詢出來,然後通過selectionKey可以取得就緒的Channel集合,從而進行後續的IO操作.
一個多路複用器(Selector)可以負責成千上萬個Channel,沒有上限,這也是JDK使用epoll代替了傳統的selector實現,獲得連接句柄沒有限制.這也意味着我們只要一個線程負責selector的輪詢,就可以接入成千上萬個客戶端,這是JDK,NIO庫的巨大進步.
來張精心整好的圖
這個學習進到深水區了,注意羅,下面是服務器端的代碼,上面例子代碼是client端的,看裏面的註解,如果還不明白,多看幾次,代碼是可運行的,記得要jdk1.7以上版本,多運行,自己意會下,我也只幫到這了!
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class Server implements Runnable{
//1 多路複用器(管理所有的通道)
private Selector seletor;
//2 建立緩衝區
private ByteBuffer readBuf = ByteBuffer.allocate(1024);
//3
private ByteBuffer writeBuf = ByteBuffer.allocate(1024);
public Server(int port){
try {
//1 打開路複用器
this.seletor = Selector.open();
//2 打開服務器通道
ServerSocketChannel ssc = ServerSocketChannel.open();
//3 設置服務器通道爲非阻塞模式
ssc.configureBlocking(false);
//4 綁定地址
ssc.bind(new InetSocketAddress(port));
//5 把服務器通道註冊到多路複用器上,並且監聽阻塞事件
ssc.register(this.seletor, SelectionKey.OP_ACCEPT);
System.out.println("Server start, port :" + port);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while(true){
try {
//1 必須要讓多路複用器開始監聽
this.seletor.select();
//2 返回多路複用器已經選擇的結果集
Iterator<SelectionKey> keys = this.seletor.selectedKeys().iterator();
//3 進行遍歷
while(keys.hasNext()){
//4 獲取一個選擇的元素
SelectionKey key = keys.next();
//5 直接從容器中移除就可以了
keys.remove();
//6 如果是有效的
if(key.isValid()){
//7 如果爲阻塞狀態
if(key.isAcceptable()){
this.accept(key);
}
//8 如果爲可讀狀態
if(key.isReadable()){
this.read(key);
}
//9 寫數據
if(key.isWritable()){
//this.write(key); //ssc
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void write(SelectionKey key){
//ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//ssc.register(this.seletor, SelectionKey.OP_WRITE);
}
private void read(SelectionKey key) {
try {
//1 清空緩衝區舊的數據
this.readBuf.clear();
//2 獲取之前註冊的socket通道對象
SocketChannel sc = (SocketChannel) key.channel();
//3 讀取數據
int count = sc.read(this.readBuf);
//4 如果沒有數據
if(count == -1){
key.channel().close();
key.cancel();
return;
}
//5 有數據則進行讀取 讀取之前需要進行復位方法(把position 和limit進行復位)
this.readBuf.flip();
//6 根據緩衝區的數據長度創建相應大小的byte數組,接收緩衝區的數據
byte[] bytes = new byte[this.readBuf.remaining()];
//7 接收緩衝區數據
this.readBuf.get(bytes);
//8 打印結果
String body = new String(bytes).trim();
System.out.println("Server : " + body);
// 9..可以寫回給客戶端數據
} catch (IOException e) {
e.printStackTrace();
}
}
private void accept(SelectionKey key) {
try {
//1 獲取服務通道
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//2 執行阻塞方法
SocketChannel sc = ssc.accept();
//3 設置阻塞模式
sc.configureBlocking(false);
//4 註冊到多路複用器上,並設置讀取標識
sc.register(this.seletor, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(new Server(7788)).start();;
}
}
如果你理解了Java NIO ,下面講的netty也是水到渠成的事,只想說,深水區已過了!
差點忘記還要補下AIO的,這個比NIO先進的技術,最終實現了
netty
這是神一樣存在的java nio框架, 這個偏底層的東西,可能你接觸較少卻又無處不在,比如:
在業界有一篇無法超越的netty入門文章,我也沒這個能力超越,只能雙手奉上,你們好好研讀,必然學有所成!
http://ifeve.com/netty5-user-guide/
還有杭州華爲的李林鋒寫的 Netty權威指南 ,我是買了一本,不知如何評論好,中等吧!
剛好,我這邊要修改的項目也是華爲的………………
未完,先發…………………………………..
————————————————
版權聲明:本文爲CSDN博主「六樓外的風景」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/yangfanend/article/details/55100327