在接下的文章中,將會分別使用Bio,Nio,Aio,Netty來實現(xiàn)時間查詢服務(wù)器,比較并分析各種版本的優(yōu)缺點。
Bio-客戶端版本
針對Bio模式下的不同的服務(wù)器版本,本節(jié)使用統(tǒng)一的客戶端版本,客戶端的處理邏輯如下:
- 根據(jù)hostname和port連接服務(wù)器 connect()方法
- 獲取socket的輸入輸出流,通過輸出流發(fā)送請求,通過輸入流讀取服務(wù)端的響應(yīng)
- 只要socket沒有斷開,就一直可以進行請求操作
代碼如下:
public class BioClient {
private String hostname;
private int port;
public BioClient(String hostname, int port) {
this.hostname = hostname;
this.port = port;
}
public void start(){
Socket socket = new Socket();
BufferedReader br = null;
BufferedWriter bw = null;
try {
//鏈接服務(wù)端
socket.connect(new InetSocketAddress(hostname, port));
br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
Scanner scanner = new Scanner(System.in);
while(true){
//請求服務(wù)端
String message = scanner.nextLine();
bw.write(message);
bw.newLine();
bw.flush();
System.out.println("發(fā)送請求:" + message);
String response = br.readLine();
System.out.println("服務(wù)端響應(yīng):" + response);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(br!=null){
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(bw!=null){
try {
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
BioClient bioClient = new BioClient("127.0.0.1", 8080);
bioClient.start();
}
}
對于上述代碼,需要注意: 通過socket的輸出流發(fā)送請求時,需要在消息末尾添加換行符,因為服務(wù)端都是根據(jù)換行符來區(qū)分每一條請求消息的。
Bio-串行接收請求版本
Bio模式下的服務(wù)器,在綁定服務(wù)端口成功之后,需要調(diào)用accept()方法,監(jiān)聽客戶端的鏈接,當客戶端經(jīng)過三次握手成功連接到服務(wù)器時,該方法才會返回,accept()方法一次只能接收一個鏈接(實際上就是去已完成鏈接隊列中取一個socket對象,上一節(jié)中已經(jīng)講過該知識點,不再累述)。下面我們就看一下在不另外開辟線程執(zhí)行連接的情景。服務(wù)端啟動的步驟如下:
- 綁定端口號,bind(),其中默認的backlog(上一節(jié)中介紹過)為50
- 調(diào)用accep()方法,監(jiān)聽端口,獲取鏈接。該方法調(diào)用一次只能返回一個連接,需要不斷的執(zhí)行才能不斷的獲取鏈接
- 處理連接,獲取鏈接的請求數(shù)據(jù),解析請求,根據(jù)請求進行響應(yīng)
代碼如下:
public class BioSimpleServer {
private int port;
public BioSimpleServer(int port) {
this.port = port;
}
public void start(){
try {
ServerSocket serverSocket = new ServerSocket(port);
while(true) {
Socket socket = serverSocket.accept();
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
String message = br.readLine();
System.out.println("接收到請求:" + message);
String response = null;
if("時間".equals(message)){
response = (new Date()).toString();
}else{
response = "該請求為錯誤請求";
}
bw.write("服務(wù)器時間為:" + response);
bw.newLine();
bw.flush();
System.out.println("服務(wù)端回復(fù)請求:" + response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
BioSimpleServer bioSimpleServer = new BioSimpleServer(8080);
bioSimpleServer.start();
}
}
對于上述代碼,對于資源的釋放并沒有關(guān)閉,只是demo而已,注意即可。有幾點需要指出:
- 將serverSocket.accept()方法一起后續(xù)操作,放在循環(huán)中,保證能夠不斷的接收新的請求
- 注意代碼中使用的是readLine()讀取數(shù)據(jù),意味著客戶端發(fā)送數(shù)據(jù)時,最后必須多添加一個換行符。用于區(qū)分不同的請求消息。
該中實現(xiàn)存在的問題:
- 如果多個客戶端請求,所有的請求都是串行執(zhí)行的,即第一個請求執(zhí)行完之后,才能接收并處理后一個請求。因此將接收到的socket扔到一個新的線程去執(zhí)行,而不阻塞當前的邏輯是非常重要的。
- 該版本,對于每一個連接只能處理一次,因為進入第二次循環(huán)的時候,輸入輸出流的對象就被改變了。這也是必須要開辟一個新線程執(zhí)行的原因。
綜上,對于Bio服務(wù)的實現(xiàn),為了不阻塞接收請求的邏輯,必須開辟一個新的線程執(zhí)行和處理連接。
Bio-IO線程版本
為了解決請求串行接收執(zhí)行的問題,我們需要開辟新的線程去執(zhí)行連接。
首先定義一個執(zhí)行socket的類,代碼如下:
public class BioRunnable implements Runnable {
private Socket socket;
private BufferedReader br;
private BufferedWriter bw;
public BioRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
while(true){
String line = br.readLine();
System.out.println("獲取客戶端請求:" + line);
//處理業(yè)務(wù)邏輯
String response = null;
if("時間".equals(line)){
response = (new Date()).toString();
}else{
response = "不能識別該請求";
}
bw.write(response);
bw.newLine();
bw.flush();
System.out.println("返回客戶端結(jié)果:" + response);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(br!=null){
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(bw!=null){
try {
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
服務(wù)端的主流程如下:
public class BioStandardServer {
private int port;
public BioStandardServer(int port) {
this.port = port;
}
public void start(){
try {
ServerSocket serverSocket = new ServerSocket(port);
while(true) {
Socket socket = serverSocket.accept();
new Thread(new BioRunnable(socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
BioStandardServer bioStandardServer = new BioStandardServer(8080);
bioStandardServer.start();
}
}
通過開辟新線程執(zhí)行處理每一個連接,服務(wù)端可以并發(fā)的處理每個請求。此外,每一個請求不再是只能發(fā)送一次請求,服務(wù)端可以處理一個連接的多次請求,只要鏈接不斷開。
該版本存在的問題:針對每一個新的鏈接,服務(wù)端都需要開辟一個新的線程執(zhí)行,服務(wù)端會存在大量線程創(chuàng)建和銷毀的過程,當客戶端較多時,線程的創(chuàng)建和銷毀的開銷是不能忽略的。因此我們將會引出下一個Bio的版本,也是我們以往最經(jīng)常使用的版本-線程池版本
Bio-線程池版本
為了避免線程頻繁的銷毀和創(chuàng)建,使用線程池來達到線程重用的效果。連接的處理類不變。只修改服務(wù)器處理的主邏輯,代碼如下:
public class BioThreadPoolServer {
private int port;
private ThreadPoolExecutor executor = new ThreadPoolExecutor(50, 50, 0, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(1000));
public BioThreadPoolServer(int port) {
this.port = port;
}
public void start(){
try {
ServerSocket serverSocket = new ServerSocket(port);
while(true) {
Socket socket = serverSocket.accept();
executor.execute(new BioRunnable(socket));
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
BioThreadPoolServer bioThreadPoolServer = new BioThreadPoolServer(8080);
bioThreadPoolServer.start();
}
}
通過使用線程池,達到了線程重用的目的
Bio的缺陷
線程池版本在并發(fā)量不大的情況下,可以很好的運行,但是當并發(fā)量過大時,其性能表現(xiàn)不佳,其主要的問題有如下幾方面:
- Bio的服務(wù)端對于連接個數(shù)有上限限制,默認為1024
- Bio為阻塞模式,當進行讀取操作,且讀取操作不滿足時(例如使用readLine()方法,找不到換行符就不會返回;例如讀出數(shù)據(jù),但數(shù)據(jù)接收隊列中沒有數(shù)據(jù),也會阻塞),操作就會則塞,阻塞時不會釋放已占有資源,對資源(例如線程資源,線程并沒有做事情,卻要一直等待)的使用造成浪費。
- Bio的處理模式會受到對方(客戶端影響服務(wù)端,服務(wù)端影響客戶端)的影響。例如客服端發(fā)送數(shù)據(jù)緩慢,就會影響到服務(wù)端對數(shù)據(jù)的接收處理,數(shù)據(jù)讀取不完,就會一直占用線程資源,與第2點本質(zhì)上是一點??蛻舳撕头?wù)端的性能相互影響,耦合性較大。
上面將的集中模式本質(zhì)上都是Bio的實現(xiàn),其中都存在Bio存在的問題。下一節(jié),將會講解Nio是如何解決Bio存在的問題。
歡迎掃描下方二維碼,關(guān)注公眾號,我們可以進行技術(shù)交流,共同成長
