Socket與NIO方式的區別

Socket是最基礎的網絡編程技術,HttpURLConnection和HttpClient都是基於Socket的。下面來看一個從服務器端下載文件到客戶端的例子。

        服務器端:      

Java代碼  
  1. package org.huodong.action;   
  2. import java.io.FileInputStream;    
  3. import java.io.InputStream;    
  4. import java.io.OutputStream;    
  5. import java.net.ServerSocket;    
  6. import java.net.Socket;    
  7. public class FilmServer {   
  8.        
  9.     public static void main(String[] args) {   
  10.         FilmServer ms = new FilmServer();   
  11.         try {   
  12.         ms.server();   
  13.         } catch (Exception e) {   
  14.         e.printStackTrace();   
  15.         }   
  16.         }   
  17.   
  18.         /**  
  19.         * 服務器端響應請求   
  20.  
  21.         標籤:   
  22.  
  23.  
  24. * @throws Exception  
  25. */  
  26. public void server() throws Exception {   
  27.   
  28. // 0.建立服務器端的server的socket   
  29. ServerSocket ss = new ServerSocket(8089);   
  30.   
  31. while (true) {   
  32.   
  33. // 1.打開socket連接   
  34. // 等待客戶端的請求   
  35. final Socket server = ss.accept(); //阻塞,一直等待直到有客戶端的請求過來,如果客戶端是兩個線程的話,那麼一定是一個線程的客戶端的連接關閉了才能接到另一個線程的客戶端的請求。   
  36.   
  37. System.out.println("服務-----------請求開始start");   
  38.   
  39. // 2.打開socket的流信息,準備下面的操作   
  40. final InputStream is = server.getInputStream();   
  41. byte b[] = new byte[1024];   
  42.   
  43. int readCount = is.read(b);   
  44.   
  45. String str = new String(b);   
  46.   
  47. str = str.trim();   
  48.   
  49. final String serverFileName = str;   
  50.   
  51. // 3.對流信息進行讀寫操作   
  52. System.out.println("客戶端傳過來的信息是:" + str);   
  53.   
  54. System.out.println("線程" + Thread.currentThread().getName() + "啓動");   
  55.   
  56. try {   
  57.   
  58. FileInputStream fileInputStream = new FileInputStream(   
  59. serverFileName);   
  60.   
  61.   
  62. OutputStream os = server.getOutputStream();   // 往客戶端寫文件   
  63.   
  64. byte[] bfile = new byte[1024];   
  65.   
  66.   
  67. while (fileInputStream.read(bfile) > 0) {   
  68. os.write(bfile);   
  69. }   
  70.   
  71. fileInputStream.close();   
  72.   
  73. os.close();   
  74.   
  75. // 4.關閉socket   
  76. // 先關閉輸入流   
  77. is.close();   
  78.   
  79. // 最後關閉socket   
  80. server.close();   
  81.   
  82. catch (Exception e) {   
  83. // TODO Auto-generated catch block   
  84. e.printStackTrace();   
  85. }   
  86.   
  87. System.out.println("服務-----------請求結束over");   
  88. }   
  89.   
  90. }   
  91.   
  92. }  
package org.huodong.action;
import java.io.FileInputStream; 
import java.io.InputStream; 
import java.io.OutputStream; 
import java.net.ServerSocket; 
import java.net.Socket; 
public class FilmServer {
	
	public static void main(String[] args) {
		FilmServer ms = new FilmServer();
		try {
		ms.server();
		} catch (Exception e) {
		e.printStackTrace();
		}
		}

		/**
		* 服務器端響應請求 

		標籤: 

* 
* @throws Exception
*/
public void server() throws Exception {

// 0.建立服務器端的server的socket
ServerSocket ss = new ServerSocket(8089);

while (true) {

// 1.打開socket連接
// 等待客戶端的請求
final Socket server = ss.accept(); //阻塞,一直等待直到有客戶端的請求過來,如果客戶端是兩個線程的話,那麼一定是一個線程的客戶端的連接關閉了才能接到另一個線程的客戶端的請求。

System.out.println("服務-----------請求開始start");

// 2.打開socket的流信息,準備下面的操作
final InputStream is = server.getInputStream();
byte b[] = new byte[1024];

int readCount = is.read(b);

String str = new String(b);

str = str.trim();

final String serverFileName = str;

// 3.對流信息進行讀寫操作
System.out.println("客戶端傳過來的信息是:" + str);

System.out.println("線程" + Thread.currentThread().getName() + "啓動");

try {

FileInputStream fileInputStream = new FileInputStream(
serverFileName);


OutputStream os = server.getOutputStream();   // 往客戶端寫文件

byte[] bfile = new byte[1024];


while (fileInputStream.read(bfile) > 0) {
os.write(bfile);
}

fileInputStream.close();

os.close();

// 4.關閉socket
// 先關閉輸入流
is.close();

// 最後關閉socket
server.close();

} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

System.out.println("服務-----------請求結束over");
}

}

}

    客戶端:  

Java代碼  
  1. package org.huodong.action;   
  2. import java.io.FileOutputStream;   
  3. import java.io.IOException;   
  4. import java.io.InputStream;   
  5. import java.io.OutputStream;   
  6. import java.net.Socket;   
  7. import java.net.UnknownHostException;   
  8. import java.util.Date;   
  9. public class FilmClient{   
  10.   
  11.   
  12.     public static void main(String[] args) {   
  13.     for (int i = 1; i <= 2; i++) {   
  14.     Client client = new Client();   
  15.     client.i = i;   
  16.     client.start();   
  17.     }   
  18.     }   
  19.     }   
  20.   
  21.     class Client extends Thread {   
  22.   
  23.     int i;   
  24.   
  25.     @Override  
  26.     public void run() {   
  27.        
  28.         Date date = new Date();   
  29.        
  30.     // 1.建立scoket連接   
  31.     Socket client;   
  32.     try {   
  33.     client = new Socket("127.0.0.1"8089);   
  34.   
  35.     // 2.打開socket的流信息,準備下面的操作   
  36.     OutputStream os = client.getOutputStream();   
  37.   
  38.     // 3.寫信息   
  39.     os.write(("d://film//音樂.rar").getBytes());   //這個是服務器端的文件地址   
  40.   
  41.     String filmName = "e://io"+i+".rar";  //這是要下載到客戶端的地址及文件名,這裏相當於下載了兩遍2.rmvb,只不過保存在客戶端的時候起了兩個不同的文件名,方便比較   
  42.   
  43.     FileOutputStream fileOutputStream = new FileOutputStream(filmName);   
  44.   
  45.   
  46.     System.out.println("Time="+date.getTime());   
  47.     InputStream is = client.getInputStream();// 接收服務器端的文件並寫到客戶端,這裏會一直等服務器端發消息過來,如果服務器sleep10秒才發送過來,客戶端也會一直等,   
  48.                                              // 這就導致整個線程都會阻塞在這裏   
  49.     byte b[] = new byte[1024];   
  50.   
  51.     while(is.read(b)>0){   
  52.     fileOutputStream.write(b);   
  53.     }   
  54.   
  55.     // 4.關閉socket   
  56.     // 先關閉輸出流   
  57.     os.close();   
  58.   
  59.     // 最後關閉socket   
  60.     client.close();   
  61.     } catch (UnknownHostException e) {   
  62.     // TODO Auto-generated catch block   
  63.     e.printStackTrace();   
  64.     } catch (IOException e) {   
  65.     // TODO Auto-generated catch block   
  66.     e.printStackTrace();   
  67.     }   
  68.     System.out.println("Time1="+date.getTime());   
  69.     }   
  70.   
  71.     }  
package org.huodong.action;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Date;
public class FilmClient{


	public static void main(String[] args) {
	for (int i = 1; i <= 2; i++) {
	Client client = new Client();
	client.i = i;
	client.start();
	}
	}
	}

	class Client extends Thread {

	int i;

	@Override
	public void run() {
	
		Date date = new Date();
	
	// 1.建立scoket連接
	Socket client;
	try {
	client = new Socket("127.0.0.1", 8089);

	// 2.打開socket的流信息,準備下面的操作
	OutputStream os = client.getOutputStream();

	// 3.寫信息
	os.write(("d://film//音樂.rar").getBytes());   //這個是服務器端的文件地址

	String filmName = "e://io"+i+".rar";  //這是要下載到客戶端的地址及文件名,這裏相當於下載了兩遍2.rmvb,只不過保存在客戶端的時候起了兩個不同的文件名,方便比較

	FileOutputStream fileOutputStream = new FileOutputStream(filmName);


	System.out.println("Time="+date.getTime());
	InputStream is = client.getInputStream();// 接收服務器端的文件並寫到客戶端,這裏會一直等服務器端發消息過來,如果服務器sleep10秒才發送過來,客戶端也會一直等,
                                             // 這就導致整個線程都會阻塞在這裏
	byte b[] = new byte[1024];

	while(is.read(b)>0){
	fileOutputStream.write(b);
	}

	// 4.關閉socket
	// 先關閉輸出流
	os.close();

	// 最後關閉socket
	client.close();
	} catch (UnknownHostException e) {
	// TODO Auto-generated catch block
	e.printStackTrace();
	} catch (IOException e) {
	// TODO Auto-generated catch block
	e.printStackTrace();
	}
	System.out.println("Time1="+date.getTime());
	}

	}

  

 客戶端啓動了2個線程進行下載電影的工作,先啓動服務端,再運行客戶端,會看筆者本地的硬盤C分區到有如下效果。

可以看到線程2的下載任務一直是0字節,等第一個線程下載完成後呢,線程2的下載任務才能進行。



服務端的代碼造成的問題就是使用傳統的sokect網絡通訊,那麼另一個客戶端的線程請求到server端的時候就發生了阻塞的情況,也就是說,服務端相當一個廁所,廁所就有隻有一個坑位,來了一個人,相當於客戶端請求,那這個人相當於就把坑位給佔了,write操作和read操作會阻塞,這個人還沒解決完問題呢,下個人就來了,沒辦法,哥們兒先在門外等等啊,等前一個客戶爽完了再給您提供服務好吧。那麼如何解決這個佔着坑位不讓別人用的情況呢?


3. 阻塞的多線程
爲了解決以上問題,那麼之後很多Server肯定不可能像以上程序那麼做,不過以前很多Server都是基於單線程服務改造一下,做成多線程的Server的通訊,修改一下上面的Server代碼,如下

Java代碼  
  1. package org.huodong.action;   
  2. import java.io.FileInputStream;   
  3. import java.io.InputStream;   
  4. import java.io.OutputStream;   
  5. import java.net.ServerSocket;   
  6. import java.net.Socket;   
  7.   
  8. /**  
  9. * 以上的Server就是在原始的socket基礎上加了線程,每一個Client請求過來後,整個Server主線程不必處於阻塞狀態,  
  10. * 接收請求後直接另起一個新的線程來處理和客戶端的交互,就是往客戶端發送二進制包。這個在新線程中雖然阻塞,  
  11. * 但是對於服務主線程沒有阻塞的影響,主線程依然通過死循環監聽着客戶端的一舉一動。  
  12. * 另一個客戶端的線程發起請求後就再起一個新的線程對象去爲客戶端服務。  
  13. */  
  14. public class FilmServerNewThread {   
  15.   
  16. public static void main(String[] args) {   
  17. FilmServerNewThread ms = new FilmServerNewThread();   
  18. try {   
  19. ms.server();   
  20. catch (Exception e) {   
  21. e.printStackTrace();   
  22. }   
  23. }   
  24.   
  25. /**  
  26. * 服務器端響應請求  
  27.  
  28. * @throws Exception  
  29. */  
  30. public void server() throws Exception {   
  31.   
  32. // 0.建立服務器端的server的socket   
  33. ServerSocket ss = new ServerSocket(8089);   
  34.   
  35. while (true) {   
  36.   
  37. // 1.打開socket連接   
  38. // 等待客戶端的請求   
  39. final Socket server = ss.accept();   
  40.   
  41. System.out.println("服務-----------請求開始start");   
  42.   
  43. // 2.打開socket的流信息,準備下面的操作   
  44. final InputStream is = server.getInputStream();   
  45. byte b[] = new byte[1024];    
  46. int readCount = is.read(b);   
  47.   
  48. String str = new String(b);   
  49.   
  50. str = str.trim();   
  51.   
  52. final String serverFileName = str;   
  53.   
  54. // 3.對流信息進行讀寫操作   
  55. System.out.println("客戶端傳過來的信息是:" + str);   
  56.   
  57. if (readCount > 0) {   
  58. new Thread() {   
  59.   
  60. @Override  
  61. public void run() {   
  62.   
  63. System.out.println("線程"  
  64. + Thread.currentThread().getName() + "啓動");   
  65.   
  66. try {   
  67.   
  68. FileInputStream fileInputStream = new FileInputStream(   
  69. serverFileName);   
  70.   
  71. // 3.1 服務器回覆客戶端信息(response)   
  72. OutputStream os = server.getOutputStream();   
  73.   
  74. byte[] bfile = new byte[1024];   
  75.   
  76. // 往客戶端寫   
  77. while (fileInputStream.read(bfile) > 0) {   
  78. os.write(bfile);   
  79. }   
  80.   
  81. fileInputStream.close();   
  82.   
  83. os.close();   
  84.   
  85. // 4.關閉socket   
  86. // 先關閉輸入流   
  87. is.close();   
  88.   
  89. // 最後關閉socket   
  90. server.close();    
  91. catch (Exception e) {   
  92. // TODO Auto-generated catch block   
  93. e.printStackTrace();   
  94. }   
  95. }   
  96. }.start();   
  97. }   
  98.   
  99. System.out.println("服務-----------請求結束over");   
  100. }   
  101.   
  102. }   
  103. }  
package org.huodong.action;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
* 以上的Server就是在原始的socket基礎上加了線程,每一個Client請求過來後,整個Server主線程不必處於阻塞狀態,
* 接收請求後直接另起一個新的線程來處理和客戶端的交互,就是往客戶端發送二進制包。這個在新線程中雖然阻塞,
* 但是對於服務主線程沒有阻塞的影響,主線程依然通過死循環監聽着客戶端的一舉一動。
* 另一個客戶端的線程發起請求後就再起一個新的線程對象去爲客戶端服務。
*/
public class FilmServerNewThread {

public static void main(String[] args) {
FilmServerNewThread ms = new FilmServerNewThread();
try {
ms.server();
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 服務器端響應請求
* 
* @throws Exception
*/
public void server() throws Exception {

// 0.建立服務器端的server的socket
ServerSocket ss = new ServerSocket(8089);

while (true) {

// 1.打開socket連接
// 等待客戶端的請求
final Socket server = ss.accept();

System.out.println("服務-----------請求開始start");

// 2.打開socket的流信息,準備下面的操作
final InputStream is = server.getInputStream();
byte b[] = new byte[1024]; 
int readCount = is.read(b);

String str = new String(b);

str = str.trim();

final String serverFileName = str;

// 3.對流信息進行讀寫操作
System.out.println("客戶端傳過來的信息是:" + str);

if (readCount > 0) {
new Thread() {

@Override
public void run() {

System.out.println("線程"
+ Thread.currentThread().getName() + "啓動");

try {

FileInputStream fileInputStream = new FileInputStream(
serverFileName);

// 3.1 服務器回覆客戶端信息(response)
OutputStream os = server.getOutputStream();

byte[] bfile = new byte[1024];

// 往客戶端寫
while (fileInputStream.read(bfile) > 0) {
os.write(bfile);
}

fileInputStream.close();

os.close();

// 4.關閉socket
// 先關閉輸入流
is.close();

// 最後關閉socket
server.close(); 
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}.start();
}

System.out.println("服務-----------請求結束over");
}

}
}

 
執行效果如下


2個線程互不影響,各自下載各自的。當然從非常嚴格的意義來講,str變量在十分高併發的情況下有線程安全問題,這個咱暫且忽略,就着眼於低併發的情況。這個問題是什麼呢,就是如果客戶端請求比較多了,那麼爲每一個客戶端開闢一個新的線程對象來處理網絡傳輸的請求,需要創建個線程對象,而且這個線程對象從時間上來講還是處於長連接,這個就比較消費系統資源,這個打開進程管理器就可以看到。而且每一個線程內部都是阻塞的,也沒有說完全利用好這個新創建的線程。還拿剛纔上廁所舉例子,好比現在不止一個坑位了,來了一個用戶我這邊就按照工程師的廁所坑位圖建立一個新的坑位,客戶來了,不用等待老坑位,用新創建的坑位就行了。等那個老坑位用完了,自然有垃圾回收器去消滅那個一次性的坑位的,騰出資源位置爲了建立新的坑位。長時間連接的意思,相當於這個人上廁所的時間非常長,便祕??需要拉一天才能爽完……老的坑位一時半會兒回收不了,新的坑位需要有空間爲其建造茅房以便滿足客戶端的“急切方便”需要。久而久之,線程數目一多,系統就掛了的概率就增多了(誰也別想上,全玩完了)。

 

NIO方式與BIO(Socket)的方式的最大區別就是NIO是一請求一線程,而BIO是一連接一線程,後者就導致如果服務器未來得及響應客戶端就要一直等一直等,該連接就一直佔用着這個線程。如果是NIO的話則可以複用連接,也就是無須等待服務器端的響應就可以繼續給服務器端發送消息,發送和接收是異步響應的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章