Socket的使用

Socket基礎(chǔ)

在說到計算機網(wǎng)絡(luò)模型的時候一定都會提到這兩個模型:OSI七層網(wǎng)絡(luò)模型和TCP/IP四層網(wǎng)絡(luò)模型,因為OSI七層過于復雜,現(xiàn)在普遍采用的是TCP/IP的四層網(wǎng)絡(luò)模型。

七層OSI & 四層TCP/IP

tcp/udp協(xié)議位于TCP/IP協(xié)議棧的傳輸層,tcp是一個面向連接、可靠的協(xié)議,而udp協(xié)議是一個不可靠的、無連接協(xié)議。剛開始我有點疑惑,什么樣的場景才需要udp協(xié)議呢?因為udp協(xié)議并不保證數(shù)據(jù)一定傳輸?shù)侥康牡兀惺裁磮鼍澳苋萑炭赡馨l(fā)生丟包的情況呢?后來在項目的某個解決方案的討論中,聽到了老大的解釋。當時我們可以選用udp和tcp方案,問我該選用什么方案。我當時也了解了一些tcp和udp,記得在網(wǎng)上看到過這么一句話,如果你需要用額外的操作保證數(shù)據(jù)準確的傳遞到目的地,那不如直接采用tcp協(xié)議,所以我說udp并不保證數(shù)據(jù)一定傳達,而tcp是可靠的,所以應(yīng)該采用tcp協(xié)議。然后老大說在局域網(wǎng)(當時要解決的就是局域網(wǎng)中的一個問題)可以認為udp是可靠的,不需要考慮丟包的情況,不過數(shù)據(jù)處理難一點,后來還是用了TCP……

Socket可以說是對TCP協(xié)議的封裝,可以理解成TCP的API,在TCP/IP協(xié)議棧中應(yīng)當屬于應(yīng)用層和傳輸層之間的抽象。那么屬于應(yīng)用層的HTTP協(xié)議,和Socket有何異同呢?HTTP協(xié)議一般來說是短連接(當然,可以指定長連接),每次請求都會建立和斷開TCP連接,而Socket默認就是長連接,兩者在傳輸層都是建立和斷開TCP連接。相比于Socket,HTTP協(xié)議顯得更加的高級。二者都是基于TCP的,Socket可以用來編寫一個HTTP框架。

Socket Client

在Java中想要使用Socket只要用Socket這個類就可以了,而且得益于Java方便的IO,入門的成本不會非常高,但是前提是你熟悉Java的IO和基本的網(wǎng)絡(luò)知識。當然了,如果你的追求更高還可以去用非阻塞的NIO玩玩……我這是不會介紹的……

接下來將會創(chuàng)建一個Socket來獲取時間(要看到結(jié)果得翻墻……)

首先創(chuàng)建一個Socket

Socket socket = new Socket("time.nist.gov",13);

第一個參數(shù)可以是主機域名也可以是ip,第二個參數(shù)是端口,這種方式建立的Socket會直接開始嘗試連接遠程服務(wù)器的端口。服務(wù)器監(jiān)聽端口的服務(wù)在和客戶端建立連接之后,會按照一定的協(xié)定發(fā)送、接收數(shù)據(jù)。而這里建立連接之后就會直接把時間返回給客戶端,這里用Java中的字符流處理,簡單~

            InputStream in = socket.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(in,"ASCII"));
            byte[] data = new byte[1024];
            int length = data.length;
            StringBuilder sb = new StringBuilder();
            String info;
            while((info = br.readLine()) != null){
                sb.append(info);
            }
            System.out.println(sb.toString());
輸出

完整代碼:

public class TimeSocket {
    public static void main(String... args){
        // 創(chuàng)建客戶端指定主機和端口
        try(Socket socket = new Socket("time.nist.gov",13)){
            socket.setSoTimeout(15000);
            InputStream in = socket.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(in,"ASCII"));
            byte[] data = new byte[1024];
            int length = data.length;
            StringBuilder sb = new StringBuilder();
            String info;
            while((info = br.readLine()) != null){
                sb.append(info);
            }
            System.out.println(sb.toString());
//            socket.shutdownOutput();

        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

Socket Client & Socket Server

接下來的例子會建立一個TCP Client 和 TCP Server,通過互相發(fā)送字符串來模擬服務(wù)器和客戶端的通信。在Java中想要使用TCP Server,可以使用ServerSocket。我們首先建立一個TCP Server:

        /**
         * 1.用指定的端口實例化一個SeverSocket對象。服務(wù)器就可以用這個端口監(jiān)聽從客戶端發(fā)來的連接請求。
         * 2.調(diào)用ServerSocket的accept()方法,以在等待連接期間造成阻塞,監(jiān)聽連接從端口上發(fā)來的連接請求。
         * 3.利用accept方法返回的客戶端的Socket對象,進行讀寫IO的操作
         * 4.關(guān)閉打開的流和Socket對象
         */
        // 1.創(chuàng)建一個服務(wù)端Socket,ServerSocket,指定綁定端口,并監(jiān)聽此端口
        ServerSocket serverSocket = new ServerSocket(10086);// 1024 - 65535的某個端口
        // 2.調(diào)用accept方法開始監(jiān)聽,等待客戶端的連接
        Socket socket = serverSocket.accept();
        // 3.獲取輸入流,并讀取客戶端信息
        InputStream is = socket.getInputStream();
        InputStreamReader isr = new InputStreamReader(is);
        BufferedReader br = new BufferedReader(isr);
        String info = null;
try {
            while ((info = br.readLine()) != null) {
                System.out.println("server: " + info);
            }
            socket.shutdownInput();
            // 4.獲取輸出流,響應(yīng)客戶端的請e求
            OutputStream os = socket.getOutputStream();
            PrintWriter pw = new PrintWriter(os);

            // 5.關(guān)閉資源
            pw.close();
            os.close();
            br.close();
            isr.close();
            is.close();
            socket.close();
            serverSocket.close();

        } catch (IOException e) {
            e.printStackTrace();
        }

注釋寫的很清楚了,這里還要提醒一下,accept()這個方法是一個阻塞方法,也就是說如果沒有socket連接,代碼會一直阻塞在這里。而接下來則是拿到輸入流,讀入客戶端的輸入。拿到客戶端的輸入之后,再獲取輸出流返回響應(yīng)給客戶端。

接下來是建立客戶端,上面的服務(wù)端監(jiān)聽了10086端口,所以下面的服務(wù)端也要連10086端口,套路跟之前的Socket差不多:

        // 客戶端
        // 1.創(chuàng)建客戶端Socket,指定服務(wù)器地址和端口
        Socket socket = new Socket("127.0.0.1", 10086);
        // 2.獲取輸出流,向服務(wù)器端發(fā)送信息
        OutputStream os = socket.getOutputStream();// 字節(jié)輸出流
        PrintWriter pw = new PrintWriter(os);// 將輸出來包裝成打印流
        pw.write("用戶名:admin;密碼:admin");
        pw.flush();
        socket.shutdownOutput();
        // 3.獲取輸入流,并讀取到服務(wù)器端的響應(yīng)信息
        InputStream is = socket.getInputStream();
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        String info = null;
        while ((info = br.readLine()) != null) {
            System.out.println("Hello,我是客戶端,服務(wù)器說:" + info);
        }
        br.close();
        is.close();
        pw.close();
        os.close();
        socket.close();

熟悉的配方,熟悉的味道,這里就不再多做介紹了。注釋寫的很詳細,運行的時候注意先運行服務(wù)端,再運行客戶端。因為服務(wù)端的代碼會阻塞在accept方法,而客戶端在服務(wù)端沒有啟動的情況下,會出現(xiàn)連接失敗的情況。

看一下server和client各自的輸出:

server
client

這里server和client的代碼我是寫在兩個類各自的main方法中的,這代碼我也是跟著網(wǎng)上找到的博客一行一行敲的,敲完之后運行,恩,成功了。后來我感覺到有點不對……不對在哪呢?因為我這里是兩個main方法運行,可以理解成是兩個進程,這兩個進程竟然驚人的出現(xiàn)了順序性!你連接我,我監(jiān)聽到,拿到你發(fā)的內(nèi)容,然后我響應(yīng)你,你再拿到我的響應(yīng)。符合邏輯,合乎情理,但是憑什么啊?多線程都要為一些個順序性撓破頭,何況是多進程?后來我仔細看了代碼,發(fā)現(xiàn)這些表現(xiàn)出來的順序性是因為:阻塞。

首先是服務(wù)端的代碼,第一次阻塞發(fā)生在accept()方法:

Socket socket = serverSocket.accept();

這里會阻塞進程,等待Socket連接。而在客戶端連接上之后,會繼續(xù)執(zhí)行代碼,代碼會在哪里阻塞呢?第二次阻塞發(fā)生在讀取客戶端輸入的時候:

        while ((info = br.readLine()) != null) {
            System.out.println("Hello,我是客戶端,服務(wù)器說:" + info);
        }

是的,就是阻塞在這個br.readLine()了,首先我們可以從代碼的現(xiàn)象來分析:從代碼來說,能正確的打印客戶端傳來的信息,那么這個while循環(huán)一定是能正常的執(zhí)行的。因為如果不滿足條件會跳出,滿足則會一直打印,即使沒讀到信息,那么只能說明一件事,br.readLine()阻塞了程序。這里的io是阻塞式io,不作更多的介紹,有興趣可以了解Java的IO模型。在讀到信息之后將之打印出來,那么后來為什么又不阻塞跳出了循環(huán)呢?這里可以看一下這個方法的注釋:

    /**
     * Reads a line of text.  A line is considered to be terminated by any one
     * of a line feed ('\n'), a carriage return ('\r'), or a carriage return
     * followed immediately by a linefeed.
     *
     * @return     A String containing the contents of the line, not including
     *             any line-termination characters, or null if the end of the
     *             stream has been reached
     *
     * @exception  IOException  If an I/O error occurs
     *
     * @see java.nio.file.Files#readAllLines
     */

注釋中關(guān)于返回值寫的非常清楚: or null if the end of the stream has been reached,如果到了流的末尾會返回null。怎么判斷到了流的末尾呢?這里我認為是這個流結(jié)束了,不會再有任何后續(xù)輸出了,這個在代碼中是怎么體現(xiàn)的呢?在Client中的這句代碼就是解釋的體現(xiàn):

socket.shutdownOutput();

終結(jié)了輸出流,這樣在Server中就跳出了while循環(huán),而不是阻塞在br.readLine。如果注釋這句shutdownOutput(),程序則會阻塞住,現(xiàn)象如下:

Server

Server沒有跟上次一樣最后有顯示退出了程序,而是一直在運行,是的阻塞住了。而Client則是沒有任何輸出,因為Client也有一個讀取的操作,也阻塞住了。

簡單的流程就是這樣,這是一個簡單的TCP Server和TCP Client的阻塞模型。接下來介紹幾種TCP Server模型。

TCP Server 的幾種模型

TCP Server有阻塞式、并發(fā)式以及異步服務(wù)器。其中阻塞式服務(wù)器最好實現(xiàn),同時也是問題比較多的。因為客戶端發(fā)送到服務(wù)器的請求,服務(wù)器會依次處理,只要碰到一個處理時間特別長的請求,其余請求就無法得到及時的處理,同時可能會有部分客戶端認為連接已經(jīng)超時了。并發(fā)式服務(wù)器在處理請求時會為連接開啟一個線程,在線程中完成各自的讀取響應(yīng)操作,這樣就不會阻塞服務(wù)器對于其他請求的訪問了,缺點無疑是服務(wù)器資源有限,不可能為每一個連接開辟新的線程,通常會配合線程池一起處理。異步服務(wù)器需要Java中的NIO配合,這里不做介紹。

阻塞式的實現(xiàn)就是上面的代碼,不過上面的代碼通過shutdown來跳出阻塞,通常來說是不會這樣的,因為shutdown之后無法再次使用輸入輸出流,一般來說會自定義數(shù)據(jù)邊界,在拿到一個完整的數(shù)據(jù)之后將數(shù)據(jù)傳遞出去,讓上層處理,然后就阻塞在那,等待下一次的數(shù)據(jù)到來。這種實現(xiàn)起來也不是非常難,跟人配合的時候協(xié)商好協(xié)議就可以了。下面要實現(xiàn)的是并發(fā)式服務(wù)器,至于異步服務(wù)器,我自己也不是非常熟,各位感興趣可以自己查閱資料。

客戶端還是使用上面的代碼,不做改變,而Server則做一些并發(fā)的處理:

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

/**
 * Created by luojun on 2017/6/5.
 * desc:熟悉Socket
 */
public class Server {

    private static Executor executor;
    private static int count = 0;

    public static void main(String... args) throws IOException {
        executor = Executors.newFixedThreadPool(100);

        /**
         * 1.用指定的端口實例化一個SeverSocket對象。服務(wù)器就可以用這個端口監(jiān)聽從客戶端發(fā)來的連接請求。
         * 2.調(diào)用ServerSocket的accept()方法,以在等待連接期間造成阻塞,監(jiān)聽連接從端口上發(fā)來的連接請求。
         * 3.利用accept方法返回的客戶端的Socket對象,進行讀寫IO的操作
         * 4.關(guān)閉打開的流和Socket對象
         */
        // 1.創(chuàng)建一個服務(wù)端Socket,ServerSocket,指定綁定端口,并監(jiān)聽此端口
        ServerSocket serverSocket = new ServerSocket(10086);// 1024 - 65535的某個端口
        // 2.調(diào)用accept方法開始監(jiān)聽,等待客戶端的連接
        new Thread(() -> {
            while (true) {
                try {
                    Socket socket = serverSocket.accept();
                    count++;
                    System.out.println("有 " + count + " 個Socket連接");
                    handleSocket(socket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    public static void handleSocket(Socket socket) {
        executor.execute(() -> {
            InputStream is = null;
            try {
                is = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(is);
                BufferedReader br = new BufferedReader(isr);
                String info = null;
                while ((info = br.readLine()) != null) {
                    System.out.println("server: " + info);
                    handleData(info);
                }
                socket.shutdownInput();
                // 4.獲取輸出流,響應(yīng)客戶端的請e求
                OutputStream os = socket.getOutputStream();
                PrintWriter pw = new PrintWriter(os);
//            if(info.equals("我還活著,你好嗎?")){
//                pw.write("我也很好,你放心。");
//                pw.flush();
//            }else {
                pw.write("歡迎您!");
                pw.flush();
//            }

                // 5.在合適的時機關(guān)閉資源
                pw.close();
                os.close();
                br.close();
                isr.close();
                is.close();
                socket.close();

            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }

    /**
     * 處理數(shù)據(jù)
     *
     * @param data 數(shù)據(jù)
     */
    public static synchronized void handleData(String data) {
    }

}

當然,這里我偷懶也沒有設(shè)置個數(shù)據(jù)邊界什么的,只是簡單的讓服務(wù)器讀取數(shù)據(jù)然后傳遞給處理方法。關(guān)于并發(fā),坑也是比較多的,只能說各位量力而行吧……下面看一下啟動多個Socket連接并發(fā)服務(wù)器時的輸出吧:

并發(fā)服務(wù)器

這里都是比較簡單的例子,希望各位看官能有耐心的看完。接下來要寫的是關(guān)于Socket失效的問題。

Socket失效

在使用Socket的過程中,有的時候會發(fā)現(xiàn)無論怎么發(fā)送數(shù)據(jù),服務(wù)器都收不到,而且Java本地也沒有任何異常提示,這個時候就需要一個機制來讓開發(fā)者判斷Socket是否失效。目前我了解的方式有兩個:心跳包和超時重發(fā),主要原理都是客戶端向服務(wù)端發(fā)送數(shù)據(jù),心跳是如果超過一段時間服務(wù)器無響應(yīng)則判定Socket失效,而超時重發(fā)可以設(shè)置為累計重發(fā)滿一定次數(shù)無響應(yīng)判斷Socket失效。心跳機制的好處是相對比較穩(wěn)定,客戶端很容易就能知道Socket是否斷開,一般來說也不是特別耗費資源,有的心跳可能會設(shè)置成十幾分鐘發(fā)送一次數(shù)據(jù)包。但是對于一些特殊的場景,需要保證Socket一定要穩(wěn)定的(比如通過中間層硬件和底層交互),那么心跳包可能會十分的頻繁,這個時候超時重發(fā)更加的適合。不過超時重發(fā)的前提是客戶端每一個數(shù)據(jù)發(fā)送都需要有服務(wù)器的響應(yīng),不然客戶端是無法判斷數(shù)據(jù)發(fā)送是否成功的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,565評論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,115評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,577評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,514評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,234評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,621評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,641評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,822評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,380評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,128評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,319評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,879評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,548評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,970評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,229評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,048評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,285評論 2 376

推薦閱讀更多精彩內(nèi)容