TCP/IP學習筆記一:BIO的網絡編程-簡單實例
標籤(空格分隔): BIO 網絡編程
一、簡單的BIO網絡編程
實例:實現簡單的web服務器(簡單模仿Tomcat的請求和響應)
服務器端的實現:
package com.bio.socket;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服務器端
* @author MOTUI
*
*/
public class BIOServerSocket {
public static void main(String[] args) throws Exception {
//1.創建serverSocket
ServerSocket serverSocket = new ServerSocket();
//2.設置端口號
serverSocket.bind(new InetSocketAddress(8989));
System.out.println("監聽端口爲8989的服務器");
//3.獲取報文
Socket socket = serverSocket.accept();
//a.獲得客戶端的意圖 request
InputStream is = socket.getInputStream();
//字符流和字節流的轉換
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
//存儲讀取的數據
StringBuilder sb = new StringBuilder();
String line = null;
//讀取數據
while((line = br.readLine()) != null){
//將數據存儲在StringBuilder中
sb.append(line);
}
System.out.println("服務器收到的數據:"+sb.toString());
//b.返回客戶端數據
OutputStream os = socket.getOutputStream();
//獲得輸出流
PrintWriter pw = new PrintWriter(os);
pw.write("接收的數據:"+sb.toString()+" 服務器已經接收到數據");
//將數據發送
pw.flush();
//關閉流
br.close();
pw.close();
socket.close();
}
}
客戶端的實現:
package com.bio.socket;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
/**
* 客戶端
* @author MOTUI
*
*/
public class BIOClientSocket {
public static void main(String[] args) throws Exception {
//1.創建Socket
Socket socket = new Socket();
//2.連接服務器
socket.connect(new InetSocketAddress("192.168.0.117",8989));
//a.發送數據到服務器
OutputStream os = socket.getOutputStream();
//獲得輸出流
PrintWriter pw = new PrintWriter(os);
pw.write("這是客戶端數據");
//將數據發送
pw.flush();
//告知服務器已經到了流的結尾
socket.shutdownOutput();
//b.獲得服務器的迴應
InputStream is = socket.getInputStream();
//字符流和字節流的轉換
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
//存儲讀取的數據
StringBuilder sb = new StringBuilder();
String line = null;
//讀取數據
while((line = br.readLine()) != null){
//將數據存儲在StringBuilder中
sb.append(line);
}
//打印服務器迴應數據
System.out.println(sb.toString());
//關閉流
br.close();
pw.close();
socket.close();
}
}
運行服務器端:
監聽端口爲8989的服務器
運行客戶端:
接收的數據:這是客戶端數據 服務器已經接收到數據
運行客戶端之後服務器端結果爲:
監聽端口爲8989的服務器
服務器收到的數據:這是客戶端數據
到此服務器端和客戶端運行結束。這樣的結果並和我們想要的結果不同,我們需要的是服務器端一直監聽客戶端發送的請求並作出響應。我們【服務器端】做如下的修改,客戶端代碼不做任何修改。
服務器端修改爲:
在執行的代碼中加上while(true)【第180行】,保證執行代碼一直執行。
package com.bio.socket;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服務器端
* @author MOTUI
*
*/
public class BIOServerSocket {
public static void main(String[] args) throws Exception {
//1.創建serverSocket
ServerSocket serverSocket = new ServerSocket();
//2.設置端口號
serverSocket.bind(new InetSocketAddress(8989));
while(true){
System.out.println("監聽端口爲8989的服務器");
//3.獲取報文
Socket socket = serverSocket.accept();
//a.獲得客戶端的意圖 request
InputStream is = socket.getInputStream();
//字符流和字節流的轉換
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
//存儲讀取的數據
StringBuilder sb = new StringBuilder();
String line = null;
//讀取數據
while((line = br.readLine()) != null){
//將數據存儲在StringBuilder中
sb.append(line);
}
System.out.println("服務器收到的數據:"+sb.toString());
//b.返回客戶端數據
OutputStream os = socket.getOutputStream();
//獲得輸出流
PrintWriter pw = new PrintWriter(os);
pw.write("接收的數據:"+sb.toString()+" 服務器已經接收到數據");
//將數據發送
pw.flush();
//關閉流
br.close();
pw.close();
socket.close();
}
}
}
運行服務器端:
監聽端口爲8989的服務器
運行客戶端:
接收的數據:這是客戶端數據 服務器已經接收到數據
運行客戶端之後服務器端結果爲:
監聽端口爲8989的服務器
服務器收到的數據:這是客戶端數據
監聽端口爲8989的服務器
到此服務器端和客戶端已經執行結束。也達到了一直監聽客戶端的請求的目的,但是還是有問題:我們處理客戶端的請求的時候會存在一個請求沒有處理完成,另一個請求不能處理。(在處理的過程中最耗時的操作就是I/O操作)。爲了解決這個問題,我們進行如下修改。
服務器端代碼修改爲:
package com.bio.socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服務器端
* @author MOTUI
*
*/
public class BIOServerSocket {
public static void main(String[] args) throws Exception {
//1.創建serverSocket
ServerSocket serverSocket = new ServerSocket();
//2.設置端口號
serverSocket.bind(new InetSocketAddress(8989));
while(true){
System.out.println("監聽端口爲8989的服務器");
//3.獲取報文
final Socket socket = serverSocket.accept();
//啓動線程
new Thread(){
public void run() {
try {
//a.獲得客戶端的意圖 request
InputStream is = socket.getInputStream();
//字符流和字節流的轉換
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
//存儲讀取的數據
StringBuilder sb = new StringBuilder();
String line = null;
//讀取數據
while((line = br.readLine()) != null){
//將數據存儲在StringBuilder中
sb.append(line);
}
System.out.println("服務器收到的數據:"+sb.toString());
//b.返回客戶端數據
OutputStream os = socket.getOutputStream();
//獲得輸出流
PrintWriter pw = new PrintWriter(os);
pw.write("接收的數據:"+sb.toString()+" 服務器已經接收到數據");
//將數據發送
pw.flush();
//關閉流
br.close();
pw.close();
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
};
}.start();
}
}
}
客戶端代碼修改爲:
package com.bio.socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
/**
* 客戶端
* @author MOTUI
*
*/
public class BIOClientSocket {
public static void main(String[] args) throws Exception {
//使用線程模擬用戶 併發訪問
for (int i = 0; i < 20; i++) {
new Thread(){
public void run() {
try {
//1.創建Socket
Socket socket = new Socket();
//2.連接服務器
socket.connect(new InetSocketAddress("192.168.0.117",8989));
//a.發送數據到服務器
OutputStream os = socket.getOutputStream();
//獲得輸出流
PrintWriter pw = new PrintWriter(os);
pw.write("這是客戶端數據");
//將數據發送
pw.flush();
//告知服務器已經到了流的結尾
socket.shutdownOutput();
//b.獲得服務器的迴應
InputStream is = socket.getInputStream();
//字符流和字節流的轉換
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
//存儲讀取的數據
StringBuilder sb = new StringBuilder();
String line = null;
//讀取數據
while((line = br.readLine()) != null){
//將數據存儲在StringBuilder中
sb.append(line);
}
//打印服務器迴應數據
System.out.println(sb.toString());
//關閉流
br.close();
pw.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
};
}.start();
}
}
}
運行服務器端:
監聽端口爲8989的服務器
運行客戶端:
接收的數據:這是客戶端數據 服務器已經接收到數據
....(省略19個:監聽端口爲8989的服務器)
運行客戶端之後服務器端結果爲:
監聽端口爲8989的服務器
....(省略17個:監聽端口爲8989的服務器)
監聽端口爲8989的服務器
當前線程ID:9
監聽端口爲8989的服務器
當前線程ID:10
服務器收到的數據:這是客戶端數據
服務器收到的數據:這是客戶端數據
當前線程ID:11
當前線程ID:12
服務器收到的數據:這是客戶端數據
服務器收到的數據:這是客戶端數據
當前線程ID:13
服務器收到的數據:這是客戶端數據
當前線程ID:14
服務器收到的數據:這是客戶端數據
當前線程ID:15
當前線程ID:17
服務器收到的數據:這是客戶端數據
服務器收到的數據:這是客戶端數據
當前線程ID:18
當前線程ID:21
當前線程ID:20
服務器收到的數據:這是客戶端數據
服務器收到的數據:這是客戶端數據
服務器收到的數據:這是客戶端數據
當前線程ID:23
當前線程ID:22
服務器收到的數據:這是客戶端數據
服務器收到的數據:這是客戶端數據
當前線程ID:24
當前線程ID:25
當前線程ID:26
服務器收到的數據:這是客戶端數據
服務器收到的數據:這是客戶端數據
服務器收到的數據:這是客戶端數據
當前線程ID:28
當前線程ID:27
服務器收到的數據:這是客戶端數據
服務器收到的數據:這是客戶端數據
當前線程ID:16
當前線程ID:19
服務器收到的數據:這是客戶端數據
服務器收到的數據:這是客戶端數據
到此運行結束,運行結果在不同的機器上有不同的順序這與機器的運行效率有關。分析到這裏我們感覺這種方式已經很合理了。在瞭解高併發之前,這的確看着不錯。但是我們可以思考一個問題:如果我們的併發打到萬級或者百萬級或者更高的時候,我們的程序有沒有問題呢?我們每有一個請求就創建一個線程,而且我們創建線程之後並沒有關注這個線程是否正確執行?是否執行結束?我們都沒有關注,這時候就出現問題了,當高併發情況下有很多的線程處於阻塞狀態,而我們的系統資源已經佔用,系統對I/O的處理就會慢,對I/O的處理變慢就會導致我們的線程阻塞,惡性循環直到系統假死(宕機)。這樣的處理是不是不合理呢。我們現在雖然無法做到線程不阻塞(不阻塞就是NIO的方式),但是我們可以進行其他的做法,我們可以控制線程的創建個數,這樣就會相對打到一個平衡。
對於我們如何控制線程創建的個數呢?我們可能會想到用計數器,javaAPI中提供了線程池的方式,使用更簡單。代碼:
服務器端修改爲:
package com.bio.socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 服務器端
* @author MOTUI
*
*/
public class BIOServerSocket {
public static void main(String[] args) throws Exception {
//1.創建serverSocket
ServerSocket serverSocket = new ServerSocket();
//2.設置端口號
serverSocket.bind(new InetSocketAddress(8989));
//創建線程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
while(true){
System.out.println("監聽端口爲8989的服務器");
//3.獲取報文
final Socket socket = serverSocket.accept();
executorService.submit(new Runnable() {
@Override
public void run() {
try {
//打印當前線程ID
System.out.println("當前線程ID:"+Thread.currentThread().getId());
//a.獲得客戶端的意圖 request
InputStream is = socket.getInputStream();
//字符流和字節流的轉換
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
//存儲讀取的數據
StringBuilder sb = new StringBuilder();
String line = null;
//讀取數據
while((line = br.readLine()) != null){
//將數據存儲在StringBuilder中
sb.append(line);
}
System.out.println("服務器收到的數據:"+sb.toString());
//b.返回客戶端數據
OutputStream os = socket.getOutputStream();
//獲得輸出流
PrintWriter pw = new PrintWriter(os);
pw.write("接收的數據:"+sb.toString()+" 服務器已經接收到數據");
//將數據發送
pw.flush();
//關閉流
br.close();
pw.close();
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
}
}
}
運行服務器端:
監聽端口爲8989的服務器
運行客戶端:
接收的數據:這是客戶端數據 服務器已經接收到數據
....(省略19個:監聽端口爲8989的服務器)
運行客戶端之後服務器端結果爲:
監聽端口爲8989的服務器
監聽端口爲8989的服務器
監聽端口爲8989的服務器
監聽端口爲8989的服務器
當前線程ID:9
監聽端口爲8989的服務器
當前線程ID:10
監聽端口爲8989的服務器
監聽端口爲8989的服務器
當前線程ID:12
服務器收到的數據:這是客戶端數據
服務器收到的數據:這是客戶端數據
當前線程ID:13
服務器收到的數據:這是客戶端數據
監聽端口爲8989的服務器
當前線程ID:11
監聽端口爲8989的服務器
服務器收到的數據:這是客戶端數據
服務器收到的數據:這是客戶端數據
當前線程ID:14
監聽端口爲8989的服務器
監聽端口爲8989的服務器
服務器收到的數據:這是客戶端數據
監聽端口爲8989的服務器
監聽端口爲8989的服務器
監聽端口爲8989的服務器
監聽端口爲8989的服務器
監聽端口爲8989的服務器
監聽端口爲8989的服務器
監聽端口爲8989的服務器
監聽端口爲8989的服務器
監聽端口爲8989的服務器
監聽端口爲8989的服務器
當前線程ID:14
服務器收到的數據:這是客戶端數據
當前線程ID:9
當前線程ID:13
當前線程ID:14
服務器收到的數據:這是客戶端數據
當前線程ID:10
當前線程ID:12
服務器收到的數據:這是客戶端數據
當前線程ID:9
服務器收到的數據:這是客戶端數據
服務器收到的數據:這是客戶端數據
服務器收到的數據:這是客戶端數據
服務器收到的數據:這是客戶端數據
當前線程ID:12
當前線程ID:10
服務器收到的數據:這是客戶端數據
當前線程ID:9
服務器收到的數據:這是客戶端數據
服務器收到的數據:這是客戶端數據
當前線程ID:15
當前線程ID:17
服務器收到的數據:這是客戶端數據
服務器收到的數據:這是客戶端數據
當前線程ID:18
當前線程ID:16
服務器收到的數據:這是客戶端數據
服務器收到的數據:這是客戶端數據
到此我們的程序就算相對完善的。在服務器端我們設置了線程的最大數爲10,所以我們的程序最大併發量爲10,這樣我們的程序就不需要一直新建線程浪費資源,只需要等待別人用完還回線程池,然後拿到繼續使用即可。但是我們的程序依然是線程阻塞的。
總結:
對於BIO的網絡編程,我們實現的是傳輸層的TCP/IP協議,這樣的傳輸效率更高。對於webService的SOP協議是Http+XML,過度的封裝使得效率上不適合對速度要求較高的應用。
多線程(線程池)實現BIO並非訪問,將IO的處理交給線程池裏的線程,限定系統線程無止境的創建節省系統資源實現線程複用.
缺點: 線程會因爲IO沒有就緒掛起.
問題分析:
上述程序屬於僞異步I/O,存在什麼樣的弊端?
/**
* Reads some number of bytes from the input stream and stores them into
* the buffer arrayb
. The number of bytes actually read is
* returned as an integer. **This method blocks until input data is
* available, end of file is detected, or an exception is thrown.**
*
*If the length of
b
is zero, then no bytes are read and
*0
is returned; otherwise, there is an attempt to read at
* least one byte. If no byte is available because the stream is at the
* end of the file, the value-1
is returned; otherwise, at
* least one byte is read and stored intob
.
* ……..
*/
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
上文中的粗體字對Socket的讀操作的說明,當Socket的輸入流進行讀操作的時候,它會一致阻塞下去,知道發生三件事:
- 有數據可讀
- 可用數據已經讀完
- 發生空指針或者I/O異常
這樣就意味着當發送方請求說這響應消息較慢,或者網絡傳輸較慢,讀取輸入流的一方的通信線程將會阻塞很長時間,如果對方60s發送完數據,讀入一方的通信線程將阻塞60s,在此期間其他接入消息將在消息隊列中排隊.