Java Socke 探究
Java中的Socket可以分為普通Socket和NioSocket兩種。
普通Socket的用法
Java中的網絡通信是通過Socket實現的,Socket分為ServerSocket和Socket兩大類,ServerSocket用于服務端,可以通過accept方法監聽請求,監聽到請求后返回Socket,Socket用于具體完成數據傳輸,客戶端直接使用Socket發起請求并傳輸數據。
一個簡單的交互介紹ServerSocket及Socket的使用:
1. 創建ServerSocket。
ServerSocket的構造方法一共有5個。最方便的是傳入一個端口參數的方法。2. 調用創建出來的ServerSocket的accept方法進行監聽
accept方法是阻塞方法,也就是說調用accept方法后程序會停下來等待連接請求,在接收到請求之前程序將不會繼續執行,當接收到請求之后,accept方法會返回一個Socket。3. 使用accept方法返回的Socket與客戶端進行通信。
服務端代碼示例:
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.text.SimpleDateFormat; import java.util.Date;public class Server {public static void main(String[] args) {try {//創建一個ServeSocket,設置端口為8080ServerSocket serverSocket = new ServerSocket(8080);//運行Socket監聽,等待請求 此方法會阻塞線程,當有請求時才會繼續執行Socket socket = serverSocket.accept();//接收到請求之后使用Socket進行通信,創建BufferedReader用于讀取請求的數據BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));PrintWriter out = new PrintWriter(socket.getOutputStream());String line = in.readLine();System.out.println(line);//創建PrintlnWriter,用于發送數據out.println("已經接受到了數據");out.flush();System.out.println("Server關閉" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.sss").format(new Date()));//關閉資源out.close();in.close();socket.close();serverSocket.close();} catch (IOException e) {e.printStackTrace();} finally {}} }客戶端代碼示例:
import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.text.SimpleDateFormat; import java.util.Date;/*** 客戶端* @author sanchan*/ public class Client {public static void main(String[] args) {//需要先啟動Server否則報錯java.net.ConnectException: Connection refusedtry {String msg="你好,ServerSocket!";//創建一個Socket,與本機8080端口連接Socket socket=new Socket("127.0.0.1",8080);//使用Socket創建PrintWriter和BufferedReader進行數據的讀寫PrintWriter out=new PrintWriter(socket.getOutputStream());out.println(msg);out.flush();BufferedReader in=new BufferedReader(new InputStreamReader(socket.getInputStream()));String line=in.readLine();System.out.println(line);//關閉資源in.close();out.close();socket.close();System.out.println("client關閉"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.sss").format(new Date()));} catch (Exception e) {e.printStackTrace();} finally {}} }NioSocket的使用
nio(new IO)是JDK1.4新增加的IO模式,nio在底層采用了與新的處理方式大大的提高了Java IO的效率。Socket也屬于IO的一種,nio提供了相對應的類:ServerSocketChannel和SocketChannel,分別對應原來的ServerSocket和Socket。
理解nio三基礎:
1. Buffer
2. Channel
3. Selector
小故事里有大智慧:
試想一下如果電商是只要有訂單就派人直接取貨去送貨【這種模式就相當于之前的Socket方式】,而不是現在的快遞模式:將貨物統一到中轉站,分揀員按照配送范圍分配快遞員,然后快遞員統一送貨【這種模式就相當于NioSocket模式】。那么你得多長時間收到你得快遞/(ㄒoㄒ)/~~
Buffer就是貨物,Channel就是快遞員,Selector就是中轉站的分揀員。
NioSocket使用步驟:
1. 創建ServerSocketChannel并設置相應參數
SerSocketChannel可以使用自身的靜態工廠方法open創建。 每個ServerSocketChannel對應一個ServerSocket,可以調用其socket方法來獲取【不過如果使用該ServerSocket監聽請求就又回到原來的 普通Socket 模式了,一般只用于使用bind方法綁定端口】。 ServerSocketChannel可以通過`SelectableChannel configureBlocking(boolean block)` 方法來設置是否采用阻塞模式。設置非阻塞模式之后可以調用register方法注冊Selector【阻塞模式不可以使用Selector】2. 創建Selector并注冊Selector到ServerSocketChannel上
Selector可以使用自身的靜態工廠方法open創建。 創建后通過上面所說的Channel的register方法注冊到ServerSocketChannel或者SocketChannel上。3.調用Selector的select方法等待請求
通過select方法等待請求,select方法可傳入代表最長等待時間的long型參數。在設定時間內接收到相應操作的請求則返回可以處理請求的數量,否則在超時后返回0,程序繼續執行。如果傳入0或者使用無參的重載方法,則會采用阻塞模式直到有相應操作的請求出現。4. 使用Selector接收請求并處理
接收到請求后Selector調用selectedKeys返回SelectionKey的Set集合。5. 使用SelectionKey獲取到Channel、Selector和操作類型并進行具體操作。
SelectionKey保存了處理當前請求的Channel和Selector,并提供了不同的操作類型。前面提到的Channel注冊Selector的register方法參數中第二個參數就是SelectionKey定義的。共有四種: SelectionKey.OP_ACCEPT //請求操作 SelectionKey.OP_CONNECT //鏈接操作 SelectionKey.OP_READ //讀操作 SelectionKey.OP_WRITE //寫操作只有在register方法中注冊了對應的操作Selector才會關心相應類型操作的請求。
Selector和Channel是多對多關系。 Selector是按不同的操作類型進行分揀,將分揀結果保存在SelectionKey中,可分別通過SelectionKey的channel、selector方法來獲取對應的Channel和Selector。可以使用SelectionKey的isAcceptable、isConnectable、isReadable和isWritable方法來判斷是什么類型的操作。Buffer是專門用于存儲數據,有四個極為重要的屬性:
capacity:容量。
Buffer最多可以保存元素的數量,創建時設置,使用過程中不可修改。
limit:可以使用的上限。
剛創建Buffer時limit等于capacity。如果給limit設置【不能超過capacity】之后,limit就成了最大可訪問的值。
例如,一個Buffer的capacity為100,表示最多可以保存100個數據,只寫入20個之后就要讀取,在讀取時limit就會設置為20。
position:當前所操作元素所在索引位置。
position從0開始,隨著get和put方法自動更新。
mark:用來暫時保存position的值。
position保存到mark之后就可以修改并進行相關的操作,操作完成后可以通過reset方法將mark的值恢復到position。
mark默認值為-1,且其值必須小于position的值。
例如,Buffer中一共保存了20個數據,position為10,現在想讀取15到20之間的數據,這時就可以調用Buffer的mark方法將目前的position保存到mark中,然后調用Buffer的position(15)將position指向第15個元素,這時就可以讀取。讀取完成之后使用Buffer的reset就可以將position恢復到10.
如果調用Buffer的position方法時傳入的值小于mark當前的值,則會將mark設為-1。
這四個屬性大小關系:mark<=position<=limit<=capacity
我們將前面的普通Socket示例的服務端改寫一下:
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.nio.charset.Charset; import java.util.Iterator;/*** @author sanchan* @since 1.0*/ public class NIOServer {public static void main(String[] args) {/*** 啟動監聽* 當監聽到請求時根據SelectionKey的操作類型交給內部類Handler進行處理*/try {//創建ServerSocketChannelServerSocketChannel ssc = ServerSocketChannel.open();//設置監聽8080端口ssc.socket().bind(new InetSocketAddress(8080));//設置為非阻塞模式ssc.configureBlocking(false);//為ServerSocketChannel注冊SelectorSelector selector = Selector.open();ssc.register(selector, SelectionKey.OP_ACCEPT);//創建HandlerHandler handler = new Handler(1024);while (true) {//等待請求,每次等待阻塞3s,超過3秒后線程繼續運行,如果傳入0或使用無參重載方法,將一直阻塞if (selector.select(3000) == 0) {System.out.println("等待請求超時~~~~~");continue;}System.out.println("處理請求~~~~~");//獲取等待處理的SelectionKeyIterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();try {//根據不同請求操作選擇對應的處理方法if (key.isAcceptable()) {handler.handleAccept(key);}if (key.isReadable()) {handler.handleRead(key);}} catch (IOException e) {e.printStackTrace();//如果異常就說明連接結束,移除keyIterator.remove();continue;}}}} catch (IOException e) {e.printStackTrace();} finally {}}/*** 請求處理類*/private static class Handler {private int bufferSize = 1024;private String localCharset = "UTF-8";public Handler() {}public Handler(int bufferSize) {this(bufferSize, null);}public Handler(String localCharset) {this(-1, localCharset);}public Handler(int bufferSize, String localCharset) {if (bufferSize > 0)this.bufferSize = bufferSize;if (localCharset != null)this.localCharset = localCharset;}/*** 處理請求操作** @param key* @throws IOException*/public void handleAccept(SelectionKey key) throws IOException {//獲取ChannelSocketChannel sc = ((ServerSocketChannel) key.channel()).accept();//設置非阻塞sc.configureBlocking(false);//注冊讀操作的Selectorsc.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));}/*** 處理讀操作** @param key* @throws IOException*/public void handleRead(SelectionKey key) throws IOException {//獲取ChannelSocketChannel sc = ((SocketChannel) key.channel());//獲取ByteBuffer/*** Buffer專門用于存儲數據,有四個極為重要的屬性:* 1. capacity:容量。* Buffer最多可以保存元素的數量,創建時設置,使用過程中不可修改。* 2. limit:可以使用的上限。* 剛創建Buffer時limit等于capacity。如果給limit設置【不能超過capacity】之后,limit就成了最大可訪問的值。* 例如,一個Buffer的capacity為100,表示最多可以保存100個數據,只寫入20個之后就要讀取,在讀取時limit就會設置為20。* 3. position:當前所操作元素所在索引位置。* position從0開始,隨著get和put方法自動更新。* 4. mark:用來暫時保存position的值。* position保存到mark之后就可以修改并進行相關的操作,操作完成后可以通過reset方法將mark的值恢復到position。* mark默認值為-1,且其值必須小于position的值。* 例如,Buffer中一共保存了20個數據,position為10,現在想讀取15到20之間的數據,這時就可以調用Buffer的mark方法將目前的position保存到mark中,然后調用Buffer的position(15)將position指向第15個元素,這時就可以讀取。讀取完成之后使用Buffer的reset就可以將position恢復到10.* 如果調用Buffer的position方法時傳入的值小于mark當前的值,則會將mark設為-1。*/ByteBuffer buffer = (ByteBuffer) key.attachment();//重置ByteBuffer。設置limit=capacity、position=0、mark=-1buffer.clear();//沒有獲取到內容則關閉if (sc.read(buffer) == -1) {sc.close();} else {/*** flip()作用:* 在保存數據時保存一個數據position加1,保存完成后要讀取數據* 就得設置limit=position,position=0**/buffer.flip();//返回數據到客戶端String receivedString = Charset.forName(localCharset).newDecoder().decode(buffer).toString();System.out.println("從客戶端獲取到了數據:" + receivedString);String sendString = "服務端已經獲取到了數據:" + receivedString;buffer = ByteBuffer.wrap(sendString.getBytes(localCharset));sc.write(buffer);//關閉SocketChannelsc.close();}}} }我是廣告
本人的直播課程在 7 月份就要開始了,希望小伙伴們支持一下,現在報名有優惠噢
https://segmentfault.com/l/15...
https://segmentfault.com/l/15...
總結
以上是生活随笔為你收集整理的Java Socke 探究的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JavaScript匿名函数以及在循环中
- 下一篇: Mac Ubuntu ----端口被占用