- java.io中最為核心的一個概念是流(Stream),面向流的編程谎倔。java中,一個流要么是輸入流猿推,要么是輸出流片习,不可能既是輸入流又是輸出流。
- Socket又稱"套接字"蹬叭,應(yīng)用程序通常通過"套接字"向網(wǎng)絡(luò)發(fā)出請求或者應(yīng)答網(wǎng)絡(luò)請求藕咏。
Socket和ServerSocket類庫位于java.net包中。ServerSocket用于服務(wù)器端秽五,Socket是創(chuàng)建網(wǎng)絡(luò)連接時使用的孽查。在連接成功時,應(yīng)用程序兩端都會產(chǎn)生一個Socket實列坦喘,操作這個實例盲再,完成所需的會話。對于一個網(wǎng)絡(luò)連接來說瓣铣,套接字是平等的答朋,
不因為在服務(wù)器端或在客戶端而產(chǎn)生不同的級別。不管是Socket還是ServerSocket它們的工作都是通過SocketImpl類及其子類完成的棠笑。 - 套接字之間的連接過程可以分為四個步驟:服務(wù)器監(jiān)聽梦碗,客戶端請求服務(wù)器,服務(wù)器確認(rèn),客戶端確認(rèn)洪规,進(jìn)行通信印屁。
- 服務(wù)器監(jiān)聽:是服務(wù)器端套接字并不定位具體的客戶端套接字,而是處于等待連接的狀態(tài)斩例,實時監(jiān)控網(wǎng)絡(luò)狀態(tài)库车。
- 客戶端請求:是指由客戶端的套接字提出連接請求,要連接的目標(biāo)是服務(wù)器端的套接字樱拴。為此,客戶端的套接字必須首先描述它要連接的服務(wù)器的套接字洋满,指出服務(wù)器端套接字的地址和端口號晶乔,然后就向服務(wù)器端套接字提出連接請求。
- 服務(wù)器端連接確認(rèn):是指當(dāng)服務(wù)器端套接字監(jiān)聽到或者說接收到客戶端套接字的連接請求牺勾,它就響應(yīng)客戶端套接字的請求正罢,建立一個新的線程,把服務(wù)器段套接字的描述發(fā)給客戶端驻民。
- 客戶端連接確認(rèn):一旦客戶端確認(rèn)了此描述翻具,連接就建立好了。雙方開始進(jìn)行通信回还。而服務(wù)器端套接字繼續(xù)處于監(jiān)聽狀態(tài)裆泳,繼續(xù)接收其他客戶端套接字的連接請求。
簡單的demo:
客戶端:
public static void main(String[] args) {
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try {
socket = new Socket("localhost", 8899);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
//向服務(wù)器端發(fā)送數(shù)據(jù)
out.println("hi server柠硕!");
out.println("我是客戶端");
String response = in.readLine();
System.out.println("Client: " + response);
} catch (Exception e) {
e.printStackTrace();
} finally {
if(in != null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(out != null){
try {
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
socket = null;
}
}
}
服務(wù)器端:
public class Server {
public static void main(String[] args) {
ServerSocket server = null;
try {
server = new ServerSocket(8899);
System.out.println("server start .. ");
//進(jìn)行阻塞
Socket socket = server.accept();
//新建一個線程執(zhí)行客戶端的任務(wù)
new Thread(new ServerHandler(socket)).start();
} catch (Exception e) {
e.printStackTrace();
} finally {
if(server != null){
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
server = null;
}
}
}
服務(wù)端handler:
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;
while(true){
body = in.readLine();
if(body == null) break;
System.out.println("Server :" + body);
out.println("服務(wù)器端回送響的應(yīng)數(shù)據(jù).");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(in != null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(out != null){
try {
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
socket = null;
}
}
}
弊端:
- 第一工禾,只能同時處理一個連接,要管理多個并發(fā)客戶端蝗柔,需要為每個新的客戶端Socket創(chuàng)建一個Thread闻葵。在這種情況下,任何時候都可能有大量的線程處于休眠狀態(tài)癣丧,只是等待輸入或者輸出數(shù)據(jù)就緒槽畔,這算是一種資源浪費(fèi)。
- 第二胁编,需要為每個線程的調(diào)用棧都分配內(nèi)存厢钧,其默認(rèn)值大小區(qū)間為64kb到1Mb,具體取決于操作系統(tǒng)掏呼。
- 即使java虛擬機(jī)(jvm)在物理上可以支持非常大數(shù)量的線程坏快,但是遠(yuǎn)在達(dá)到該極限之前,上下文切換所帶來的開銷就會帶來麻煩憎夷,例如莽鸿,在達(dá)到10000個連接的時候。
1.1.1 Java NIO
- NIO開始的時候是新的輸入/輸出(New Input/Output)的英文縮寫,也可以被稱作非阻塞(Non-blocking I/O)祥得。
- 阻塞:應(yīng)用程序在獲取網(wǎng)絡(luò)數(shù)據(jù)的時候兔沃,如果網(wǎng)絡(luò)傳輸數(shù)據(jù)很慢,那么程序就一直等著级及,直到傳輸完畢為止乒疏。
- 非阻塞:應(yīng)用程序直接可以獲取已經(jīng)準(zhǔn)備就緒好的數(shù)據(jù),無需等待饮焦。
我們都是把數(shù)據(jù)從Channel讀到Buffer中怕吴,然后再從Buffer中讀到程序中,絕對不可能數(shù)據(jù)從Channel直接讀到程序中县踢。
IO中一個流不可能既是輸入流又是輸出流转绷,Channel把數(shù)據(jù)讀到Buffer,buffer將數(shù)據(jù)讀到程序中硼啤,程序也可以將數(shù)據(jù)寫到buffer中议经。除了數(shù)組之外,Buffer還提供了對于數(shù)據(jù)的結(jié)構(gòu)化訪問方式谴返,并且可以追蹤到系統(tǒng)的讀寫過程煞肾。
Java中的7種原生數(shù)據(jù)類型都有各自對應(yīng)的Buffer類型,如IntBuffer嗓袱,LongBuffer籍救,ByteBuffer,CharBuffer等等,并沒有BooleanBuffer類型渠抹。
Channel指的是可以向其寫入數(shù)據(jù)或是從中讀取數(shù)據(jù)的對象钧忽,它類似于java.io中的stream。
所有數(shù)據(jù)的讀寫都是通過Buffer來進(jìn)行的逼肯,永遠(yuǎn)不會出現(xiàn)直接向Channel寫入數(shù)據(jù)的情況耸黑,或是直接從Channel讀取數(shù)據(jù)的情況。
與Stream不同的是篮幢,Channel是雙向的大刊,一個流只可能是InputStream或是OutputStream,Channel打開后則可以進(jìn)行讀寫三椿,寫入或是讀寫缺菌。
由于Channel是雙向的,因此它能更好的反映出底層操作系統(tǒng)的真實情況搜锰,在Linux系統(tǒng)中伴郁,底層操作系統(tǒng)的通道就是雙向的。
NIO三個最重要的組件
Buffer(緩沖區(qū))蛋叼,Channer(管道焊傅,通道) Selector(選擇器剂陡,多路復(fù)用器)
Buffer:Buffer是一個對象,它包含一些要寫入或者要讀取的數(shù)據(jù)狐胎。在NIO類庫中加入Buffer對象鸭栖,體現(xiàn)了NIO與原IO的一個重要的區(qū)別。在面向流的IO中握巢,可以將數(shù)據(jù)直接寫入或讀取到Stream對象中晕鹊。在NIO庫中,所有數(shù)據(jù)都是用緩存區(qū)處理的(讀寫)暴浦。緩沖區(qū)實質(zhì)上是一個數(shù)組溅话,通常它是一個字節(jié)數(shù)組(ByteBuffer),也可以使用其它類型的數(shù)組歌焦。這個數(shù)組為緩沖區(qū)提供了數(shù)組的訪問讀寫等操作屬性公荧,如position,capacity同规,limit等概念。
Buffer類型:我們最常用的就是ByteBuffer窟社,實際上每一種java基本類型都對于了一種緩存區(qū)(除了Boolean型),ByteBuffer,CharBuffer,ShortBuffer,IntBuffer
,LongBuffer,FloatBuffer,DoubleBufferChannel:
通道(Channel)券勺,網(wǎng)絡(luò)數(shù)據(jù)通過Channel讀取和寫入,通道與流不同之處在于通道是雙向的灿里,而流只是一個方向上移動(一個流必須是InputStream或者OutputStream的子類)关炼,而通道可以用于讀,寫或者二者同時進(jìn)行匣吊,最關(guān)鍵的是可以與多路復(fù)用器結(jié)合起來儒拂,有多種的狀態(tài)位,方便多路復(fù)用器去識別色鸳。事實上通道分為兩大類社痛,一類是網(wǎng)絡(luò)讀寫的(SelectableChannel),一類是用于文件操作的(FileChannel),我們使用的SocketChannel和ServerSocketChannel都是SelectableChannel的子類命雀。Selector:
多路復(fù)用器(Selector)蒜哀,他是NIO編程的基礎(chǔ),非常重要吏砂。多路復(fù)用器提供選擇已經(jīng)就緒的任務(wù)的能力撵儿。
簡單來說,就是Selector會不斷的輪詢注冊在其上的通道(Channel),如果某個通道發(fā)生了讀寫操作狐血,這個通道就處于就緒狀態(tài)淀歇,會被Selector輪詢出來,然后通過SelectKey可以取得就緒的Channel集合匈织,從而進(jìn)行后續(xù)的IO操作浪默。
一個多路復(fù)用器(Selector)可以負(fù)責(zé)成千上萬Channel通道,沒有上限,這也是JDK使用了epoll代替了傳統(tǒng)的select實現(xiàn)浴鸿,獲得連接句柄沒有限制井氢。這也就意味著我們只要一個線程負(fù)責(zé)Selector的輪詢,就可以接入成千上萬個客戶端岳链,這是JDK NIO庫的巨大進(jìn)步花竞。
Selector線程就類似一個管理者(Master),管理了成千上萬個管理掸哑,然后輪詢那個通道的數(shù)據(jù)已經(jīng)準(zhǔn)備好约急,通知cpu執(zhí)行io的讀取或?qū)懭氩僮鳌?/p>
Selector模式:當(dāng)IO事件(管道)注冊到選擇器以后,selector會分配給每個管道一個key值苗分,相當(dāng)于標(biāo)簽厌蔽。selector選擇器是以輪詢的方式進(jìn)行查找注冊的所有IO事件(管道),當(dāng)我們的IO時間(管道)準(zhǔn)備就緒后摔癣,select就會識別奴饮,會通過key值來找到相應(yīng)的管道,進(jìn)行相關(guān)的數(shù)據(jù)處理操作(從管道里讀或?qū)憯?shù)據(jù)择浊,寫到我們的數(shù)據(jù)緩沖區(qū)中)戴卜。
Demo1
了解一下Buffer概念
import java.nio.IntBuffer;
import java.security.SecureRandom;
public class NioTest1 {
public static void main(String[] args) {
//IntBuffer緩沖區(qū)分配10個長度
IntBuffer buffer = IntBuffer.allocate(10);
//將數(shù)據(jù)寫到buffer中
for(int i=0;i<buffer.capacity();i++){
int randomNumber = new SecureRandom().nextInt(20);
buffer.put(randomNumber);
}
//使用flip實現(xiàn)讀寫的切換
buffer.flip();
//將buffer數(shù)據(jù)讀出來
while (buffer.hasRemaining()){
System.out.println(buffer.get());
}
}
}
Demo2
管道與Buffer結(jié)合使用,我們都是將數(shù)據(jù)從Channel讀到Buffer中(涉及到Buffer就是寫動作)琢岩,然后Buffer讀到程序中投剥,絕對不可能數(shù)據(jù)直接從Channel直接讀到程序中。
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NioTest2 {
public static void main(String[] args) throws Exception{
FileInputStream fileInputStream = new FileInputStream("NioTest2.txt");
FileChannel fileChannel = fileInputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
fileChannel.read(byteBuffer);
//定義程序的反轉(zhuǎn)担孔,由讀到寫
byteBuffer.flip();
while(byteBuffer.remaining() > 0){
byte b = byteBuffer.get();
System.out.println("Character: "+(char)b);
}
fileInputStream.close();
}
}
Demo3
import java.nio.IntBuffer;
import java.security.SecureRandom;
public class NioTest1 {
public static void main(String[] args) {
//IntBuffer緩沖區(qū)分配10個長度
IntBuffer buffer = IntBuffer.allocate(10);
System.out.println("capacity:"+buffer.capacity());
//將數(shù)據(jù)寫到buffer中
for(int i=0;i<5;i++){
int randomNumber = new SecureRandom().nextInt(20);
buffer.put(randomNumber);
}
System.out.println("before flip limit: "+buffer.limit()); //10
//使用flip實現(xiàn)讀寫的切換
buffer.flip();
System.out.println("after flip limit: "+buffer.limit()); //5
System.out.println("enter while loop");
//將buffer數(shù)據(jù)讀出來
while (buffer.hasRemaining()){
System.out.println("position: "+buffer.position());
System.out.println("limit: "+buffer.limit()); //5
System.out.println("capacity: "+buffer.capacity()); //10
System.out.println(buffer.get());
}
}
}
關(guān)于NIO Buffer中的3個重要狀態(tài)屬性的含義:position江锨,limit與capacity。flip方法為什么在讀寫轉(zhuǎn)換的時候調(diào)用糕篇。
java的api對其的描述:
A buffer is a linear, finite sequence of elements of a specific primitive type. Aside from its content, the essential properties of a buffer are its capacity, limit, and position:
一個buffer是線性啄育,有限的特性原生類型的序列。除了其內(nèi)容外拌消,一個buffer最基礎(chǔ)的屬性是capacity灸撰,limit,和position:
A buffer's capacity is the number of elements it contains. The capacity of a buffer is never negative and never changes.
一個buffer的capacity就是其包含的元素的數(shù)量拼坎。一個buffer的capacity不可能是負(fù)數(shù)并且不會被改變浮毯。
A buffer's limit is the index of the first element that should not be read or written. A buffer's limit is never negative and is never greater than its capacity.
一個buffer的limit指的是無法再去讀或者寫的下一個元素的索引。一個buffer的limit不可能是負(fù)數(shù)并且不可能大于它的capacity泰鸡。
A buffer's position is the index of the next element to be read or written. A buffer's position is never negative and is never greater than its limit.
一個buffer的position是下個能被讀或者寫的元素的索引债蓝。一個buffer的position不可能是負(fù)數(shù)并且不可能大于它的limit。
- 調(diào)用buffer.flip()方法設(shè)置了當(dāng)前的limit的值等于當(dāng)前的position芳誓,并且將position的值設(shè)為0.
- 調(diào)用buffer.clear()方法設(shè)置limit的值等于capacity并且position的值設(shè)為0。
Demo4
寫一個關(guān)于三個組件都使用的程序
public class NioTest4 {
public static void main(String[] args) throws Exception{
int[] ports = new int[5];
ports[0]= 5000;
ports[1]= 5001;
ports[2]= 5002;
ports[3]= 5003;
ports[4]= 5004;
//Selector對象最常見的構(gòu)建方式
Selector selector = Selector.open();
for (int i = 0; i <ports.length ; i++) {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//設(shè)置channel為非阻塞模式
serverSocketChannel.configureBlocking(false);
//得到與channel相關(guān)聯(lián)的Socket
ServerSocket serverSocket = serverSocketChannel.socket();
InetSocketAddress inetSocketAddress = new InetSocketAddress(ports[i]);
serverSocket.bind(inetSocketAddress);
//將channel注冊到selector中啊鸭,并且監(jiān)聽的是接收連接事件锹淌,這邊只能監(jiān)聽客戶端連接事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("監(jiān)聽端口:" + ports[i]);
}
while(true){
int number = selector.select();
System.out.println("number: "+number);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
System.out.println("selectedKeys: "+selectionKeys);
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while(iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
//接收到連接,我們將selector注冊到一個個的ServerChannel當(dāng)中赠制,我們可以拿到每一個與之連接的Channel
if(selectionKey.isAcceptable()){
ServerSocketChannel serverSocketChannel = (ServerSocketChannel)selectionKey.channel();
//得到連接這個channel的SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
//向selector注冊可讀事件
socketChannel.register(selector,SelectionKey.OP_READ);
//處理完selectkey之后一定要remove掉
iterator.remove();
System.out.println("獲得客戶端的連接: "+socketChannel);
}else if(selectionKey.isReadable()){
SocketChannel socketChannel= (SocketChannel)selectionKey.channel();
int bytesRead = 0;
while(true){
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
byteBuffer.clear();
int read = socketChannel.read(byteBuffer);
if(read <= 0){
break;
}
byteBuffer.flip();
socketChannel.write(byteBuffer);
bytesRead +=read;
}
System.out.println("讀嚷赴凇: "+ bytesRead+",來自于: "+socketChannel);
iterator.remove();
}
}
}
}
}
通過nc localhost 5050
,nc localhost 5051
钟些,nc localhost 5052
等可以去測試.
Selector維護(hù)著三個set集合:
第一個就是所有當(dāng)前注冊到selector上的key的集合烟号。這個set可以通過調(diào)用keys()方法來獲得。
第二個就是確定的一種感興趣的集合比如連接的動作政恍,可讀可寫的動作汪拥,這個set是通過selectedKeys方法返回的,這個已經(jīng)被選擇的key集合總是上面的keys()方法返回的子集篙耗。
第三個就是取消的key的集合是已經(jīng)被取消的集合但是其channels還沒有被取消注冊迫筑。這個set集合是不能直接訪問的。這個已經(jīng)取消的key的集合也是上面的key set集合的一個子集宗弯。一般取消的集合都要在當(dāng)前的遍歷中刪除掉脯燃。