Java Socke 探究

Java中的Socket能夠分爲普通Socket和NioSocket兩種。

普通Socket的用法

Java中的網絡通訊是經過Socket實現的,Socket分爲ServerSocket和Socket兩大類,ServerSocket用於服務端,能夠經過accept方法監聽請求,監聽到請求後返回Socket,Socket用於具體完成數據傳輸,客戶端直接使用Socket發起請求並傳輸數據。java

一個簡單的交互介紹ServerSocket及Socket的使用:

1. 建立ServerSocket。

ServerSocket的構造方法一共有5個。最方便的是傳入一個端口參數的方法。

2. 調用建立出來的ServerSocket的accept方法進行監聽

accept方法是阻塞方法,也就是說調用accept方法後程序會停下來等待鏈接請求,在接收到請求以前程序將不會繼續執行,當接收到請求以後,accept方法會返回一個Socket。

3. 使用accept方法返回的Socket與客戶端進行通訊。

服務端代碼示例:segmentfault

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,設置端口爲8080
            ServerSocket 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 refused
        try {
            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。socket

理解nio三基礎:

1. Buffer

2. Channel

3. Selector

小故事裏有大智慧:

試想一下若是電商是隻要有訂單就派人直接取貨去送貨【這種模式就至關於以前的Socket方式】,而不是如今的快遞模式:將貨物統一到中轉站,分揀員按照配送範圍分配快遞員,而後快遞員統一送貨【這種模式就至關於NioSocket模式】。那麼你得多長時間收到你得快遞/(ㄒoㄒ)/~~
Buffer就是貨物,Channel就是快遞員,Selector就是中轉站的分揀員。this

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纔會關心相應類型操做的請求。.net

Selector和Channel是多對多關係。
Selector是按不一樣的操做類型進行分揀,將分揀結果保存在SelectionKey中,可分別經過SelectionKey的channel、selector方法來獲取對應的Channel和Selector。可使用SelectionKey的isAcceptable、isConnectable、isReadable和isWritable方法來判斷是什麼類型的操做。

Buffer是專門用於存儲數據,有四個極爲重要的屬性:

  1. capacity:容量。
    Buffer最多能夠保存元素的數量,建立時設置,使用過程當中不可修改。線程

  2. limit:可使用的上限。
    剛建立Buffer時limit等於capacity。若是給limit設置【不能超過capacity】以後,limit就成了最大可訪問的值。code

例如,一個Buffer的capacity爲100,表示最多能夠保存100個數據,只寫入20個以後就要讀取,在讀取時limit就會設置爲20。orm

  1. position:當前所操做元素所在索引位置。
    position從0開始,隨着get和put方法自動更新。server

  2. 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 {
            //建立ServerSocketChannel
            ServerSocketChannel ssc = ServerSocketChannel.open();
            //設置監聽8080端口
            ssc.socket().bind(new InetSocketAddress(8080));
            //設置爲非阻塞模式
            ssc.configureBlocking(false);
            //爲ServerSocketChannel註冊Selector
            Selector selector = Selector.open();
            ssc.register(selector, SelectionKey.OP_ACCEPT);
            //建立Handler
            Handler handler = new Handler(1024);
            while (true) {
                //等待請求,每次等待阻塞3s,超過3秒後線程繼續運行,若是傳入0或使用無參重載方法,將一直阻塞
                if (selector.select(3000) == 0) {
                    System.out.println("等待請求超時~~~~~");
                    continue;
                }
                System.out.println("處理請求~~~~~");
                //獲取等待處理的SelectionKey
                Iterator<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 {
            //獲取Channel
            SocketChannel sc = ((ServerSocketChannel) key.channel()).accept();
            //設置非阻塞
            sc.configureBlocking(false);
            //註冊讀操做的Selector
            sc.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
        }

        /**
         * 處理讀操做
         *
         * @param key
         * @throws IOException
         */
        public void handleRead(SelectionKey key) throws IOException {
            //獲取Channel
            SocketChannel 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=-1
            buffer.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);
                //關閉SocketChannel
                sc.close();
            }

        }
    }
}

我是廣告

本人的直播課程在 7 月份就要開始了,但願小夥伴們支持一下,如今報名有優惠噢

https://segmentfault.com/l/15...

https://segmentfault.com/l/15...

相關文章
相關標籤/搜索