初探Java Socket

前言

本篇文章將涉及如下內容:html

  • IO實現Java Socket通訊
  • NIO實現Java Socket通訊

閱讀本文以前最好了解過:java

  • Java IO
  • Java NIO
  • Java Concurrency
  • TCP/IP協議

TCP 套接字

TCP套接字是指IP號+端口號來識別一個應用程序,從而實現端到端的通信。其實一個套接字也能夠被多個應用程序使用,可是一般來講承載的是一個應用程序的流量。創建在TCP鏈接之上最著名的協議爲HTTP,咱們平常生活中使用的瀏覽器訪問網頁一般都是使用HTTP協議來實現的。面試

先來了解一下經過TCP套接字實現客戶端和服務器端的通訊。編程

在TCP客戶端發出請求以前,服務器會建立新的套接字(socket),並將套接字綁定到某個端口上去(bind),默認狀況下HTTP服務的端口號爲80。綁定完成後容許套接字進行鏈接(listen)並等待鏈接(accept)。這裏的accept方法會掛起當前的進程直到有Socket鏈接。瀏覽器

在服務器準備就緒後,客戶端就能夠發起Socket鏈接。客戶端獲取服務器的Socket套接字(IP號:端口號),並新建一個本地的套接字。而後連同本地的套接字發送到服務器上。緩存

服務器accept該請求並讀取該請求。這裏麪包括有TCP的三次鏈接過程。鏈接創建以後,客戶端發送HTTP請求並等待響應。服務端根據HTTP報文返回響應,並關閉鏈接。服務器

Web Server

當下的Web服務器可以同時支持數千條鏈接,一個客戶端可能向服務器打開一條或多條鏈接,這些鏈接的使用狀態各不相同,使用率也差別很大。如何有效的利用服務器資源提供低延時的服務成了每一個服務器都須要考慮的問題。根據服務器的處理方式,能夠分爲如下4種服務器,咱們也將分別對其進行簡單的實現。微信

  • 單線程服務器
  • 多進程及多線程服務器
  • 複用IO服務器
  • 複用的多線程服務器

單線程服務器

一次只處理一個請求,直到其完成爲止。一個事務處理結束後,纔會去處理下一條鏈接。實現簡單,可是性能堪憂。多線程

多進程及多線程服務器

能夠根據須要建立,或預先建立一下線程/進程。能夠爲每條鏈接分配一個線程/進程。可是當強求數量過多時,過多的線程會致使內存和系統資源的浪費。併發

複用I/O服務器

在複用結構中,會同時監視全部鏈接上的活動,當鏈接狀態發生變化時,就對那條鏈接進行少許的處理。處理結束後,就將鏈接返回到開放鏈接列表中,等待下一次狀態的變化。以後在有事情可作時纔會對鏈接進行處理。在空閒鏈接上等待的時候不會綁定線程和進程。

複用的多線程服務器

多個線程(對應多個CPU)中的每個都在觀察打開的鏈接(或是打開鏈接中的一個子集)。並對每條鏈接的狀態變化時執行任務。

Socket通訊基本實現

根據咱們上面講述的Socket通訊的步驟,在Java中咱們能夠按照如下方式逐步創建鏈接:

首先開啓服務器端的SocketServer而且將其綁定到一個端口等待Socket鏈接:

ServerSocket serverSocket = new ServerSocket(PORT_ID:int);
Socket socket = serverSocket.accept();

當沒有Socket鏈接時,服務器會在accept方法處阻塞。

而後咱們在客戶端新建一個Socket套接字而且鏈接服務器:

Socket socket = new Socket(SERVER_SOCKET_IP, SERVER_SOCKET_PORT);
socket.setSoTimeout(100000);

若是鏈接失敗的話,將會拋出異常說明服務器當前不可使用。
鏈接成功給的話,客戶端就能夠獲取Socket的輸入流和輸出流併發送消息。寫入Socket的輸出流的信息將會先存儲在客戶端本地的緩存隊列中,知足必定條件後會flush到服務器的輸入流。服務器獲取輸入後能夠解析輸入的數據,而且將響應內容寫入服務器的輸出流並返回客戶端。最後客戶端從輸入流讀取數據。

客戶端獲取Socket輸入輸出流,這裏將字節流封裝爲字符流。

//獲取Socket的輸出流,用來發送數據到服務端
PrintStream out = new PrintStream(socket.getOutputStream());
//獲取Socket的輸入流,用來接收從服務端發送過來的數據
BufferedReader buf =  new BufferedReader(new InputStreamReader(socket.getInputStream()));

客戶端發送數據並等待響應

String str = "hello world";
out.println(str);
String echo = buf.readLine();
System.out.println("收到消息:" + echo);

這裏須要注意的是,IO流是阻塞式IO,所以在讀取服務端響應的過程當中(即buf.reaLine()這一行)會阻塞直到收到服務器響應。

客戶端發送結束以後不要忘了關閉IO和Socket通訊

out.close();
buf.close();
socket.close();

服務器對消息的處理和客戶端相似,後面會貼上完整代碼。

Java Socket通訊阻塞式通訊實現

這裏咱們對上述的理論進行簡單的實現。這裏咱們實現一個簡單的聊天室,只不過其中一方是Server角色而另外一個爲Client角色。兩者都經過System.in流輸入數據,併發送給對方。正如咱們前面所說,IO流的通訊是阻塞式的,所以在等待對方響應的過程當中,進程將會掛起,咱們這時候輸入的數據將要等到下一輪會話中才能被讀取。

client端

import java.io.*;
import java.net.Socket;
import java.net.SocketTimeoutException;

public class SocketClient {

    public static void send(String server, int port){
        try {
            Socket socket = new Socket(server, port);
            socket.setSoTimeout(100000);

            System.out.println("正在鏈接服務器");

            //從控制檯讀入數據
            BufferedReader input = new BufferedReader(new InputStreamReader(System.in));

            //獲取Socket的輸出流,用來發送數據到服務端
            PrintStream out = new PrintStream(socket.getOutputStream());
            //獲取Socket的輸入流,用來接收從服務端發送過來的數據
            BufferedReader buf =  new BufferedReader(new InputStreamReader(socket.getInputStream()));
            boolean running = true;
            while(running){
                System.out.print("輸入信息:");
                String str = input.readLine();
                out.println(str);

                if("bye".equals(str)){
                    running = false;
                }else{
                    try{
                        //從服務器端接收數據有個時間限制(系統自設,也能夠本身設置),超過了這個時間,便會拋出該異常
                        String echo = buf.readLine();
                        System.out.println("收到消息:" + echo);
                    }catch(SocketTimeoutException e){
                        System.out.println("Time out, No response");
                    }
                }
            }

            input.close();
            socket.close();

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

    public static void main(String[] args){
        send("127.0.0.1", 2048);
    }
}

Server端

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketServer {

    public static void main(String[] args) throws IOException {
        //服務端在2048端口監聽客戶端請求的TCP鏈接
        ServerSocket server = new ServerSocket(2048);
        Socket client = null;
        boolean f = true;
        while(f){
            //等待客戶端的鏈接,若是沒有獲取鏈接
            client = server.accept();
            System.out.println("與客戶端鏈接成功!");
            //爲每一個客戶端鏈接開啓一個線程
            new Thread(new ServerThread(client)).start();

        }
        server.close();
    }

}

服務器處理數據

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;

public class ServerThread implements Runnable{
    private Socket client = null;
    public ServerThread(Socket client){
        this.client = client;
    }

    @Override
    public void run() {
        try{
            //獲取Socket的輸出流,用來向客戶端發送數據
            PrintStream out = new PrintStream(client.getOutputStream());

            //獲取Socket的輸入流,用來接收從客戶端發送過來的數據
            BufferedReader buf = new BufferedReader(new InputStreamReader(client.getInputStream()));

            BufferedReader serverResponse = new BufferedReader(new InputStreamReader(System.in));
            boolean flag =true;
            while(flag){
                //接收從客戶端發送過來的數據
                String str =  buf.readLine();
                System.out.println("收到消息:" + str);
                if(str == null || "".equals(str)){
                    flag = false;
                }else{
                    if("bye".equals(str)){
                        flag = false;
                    }else{
                        //將接收到的字符串前面加上echo,發送到對應的客戶端
                        System.out.print("發送回覆:");
                        String response  = serverResponse.readLine();
                        out.println(response);
                    }
                }
            }
            out.close();
            client.close();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

能夠和小夥伴試試看,分別啓動SocketServerSocketClient並進行通訊。不過前提是大家兩個須要在一個局域網中。

Java實現單線程服務器

上面的服務器其實只在主線程監聽了一個Socket鏈接,並在30秒以後將其自動關閉了。咱們將實現一個經典的單線程服務器。原理和上面類似,這裏咱們能夠直接經過向服務器發送HTTP請求來驗證該服務器的運行。

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class SingleThreadServer implements Runnable{

    private ServerSocket serverSocket;

    public SingleThreadServer(ServerSocket serverSocket){
        this.serverSocket = serverSocket;
    }
    @Override
    public void run() {
        Socket socket = null;
        try{
            while (!Thread.interrupted()){
                socket = serverSocket.accept();

                //谷歌瀏覽器每次會發送兩個請求
                //一次用於獲取html
                //一次用於獲取favicon
                //若是獲取favicon成功就緩存,不然會一直請求得到favicon
                //而火狐瀏覽器第一次也會發出這兩個請求
                //在得到favicon失敗後就不會繼續嘗試獲取favicon
                //所以使用谷歌瀏覽器訪問該Server的話,你會看到 鏈接成功 被打印兩次
                System.out.println("鏈接成功");
                process(socket);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
                serverSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void process(Socket socket){
        try {

            InputStreamReader inputStreamReader = null;
            BufferedOutputStream bufferedOutputStream = null;
            try{
                inputStreamReader = new InputStreamReader(socket.getInputStream());
                bufferedOutputStream = new BufferedOutputStream(socket.getOutputStream());

                //這裏沒法正常讀取輸入流,由於在沒有遇到EOF以前,流會任務socket輸入還沒有結束,將會繼續等待直到socket中斷
                //因此這裏咱們將暫時不讀取Socket的輸入流中的內容。
                //int size;
                //char[] buffer = new char[1024];
                //StringBuilder stringBuilder = new StringBuilder();
                //while ((size = inputStreamReader.read(buffer)) > 0){
                  //  stringBuilder.append(buffer, 0, size);
                //}


                byte[] responseDocument = "<html><body> Hello World </body></html>".getBytes("UTF-8");
                byte[] responseHeader = ("HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Length: " + responseDocument.length + "\r\n\r\n").getBytes("UTF-8");

                bufferedOutputStream.write(responseHeader);
                bufferedOutputStream.write(responseDocument);
            }finally {
                bufferedOutputStream.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

該服務器用單一線程處理每次請求,每一個線程都將等待服務器處理完上一個請求以後才能得到響應。這裏須要注意,純HTTP請求的輸入流的讀取會遇到輸入流阻塞的問題,由於HTTP請求並無輸入流可識別的EOF標記。從而致使服務器一直掛起在讀取輸入流的地方。它的解決方法以下:

  • 客戶端關閉Socket鏈接,強制服務器關閉該Socket鏈接。可是同時也丟失服務器響應
  • 自定義協議,從而服務器能夠識別數據的終點。

啓動服務器

public static void main(String[] args) throws IOException, InterruptedException {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        ServerSocket serverSocket = new ServerSocket(2048);
        executorService.execute(new SingleThreadServer(serverSocket));

//        TimeUnit.SECONDS.sleep(10);
//        System.out.println("shut down server");
//        executorService.shutdownNow();
    }

注意要先關閉以前佔用2048端口號的服務器。

咱們也可使用代碼來測試:

import java.io.*;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class TestSingleThreadServer {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0 ; i<10 ; i++){
            final int threadId = i;
            executorService.execute(() ->{

                try {
                    Socket socket = new Socket("127.0.0.1", 20006);
                    socket.setSoTimeout(5000);

                    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    String line = bufferedReader.readLine();

                    System.out.println(threadId + ":" + line);

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

            });

        }

        TimeUnit.SECONDS.sleep(40);
        executorService.shutdownNow();
    }
}

Java實現多線程服務器

這裏咱們將爲每個Socket鏈接提供一個線程來處理。基本實現和上面差很少,只是將每個Socket鏈接丟給一個額外的線程來處理。這裏能夠參考前面的簡易聊天室來試着本身實現如下。

Java NIO實現複用服務器

NIO的出現改變了舊式Java讀取IO流的方式。首先,它支持非阻塞式讀取,其次它可使用一個線程來管理多個信道。多線程表面上看起來能夠同時處理多個Socket通訊,可是多線程的管理自己也消耗至關多的資源。其次,不少信道的使用率每每並不高,一些信道每每並非連通狀態中。若是咱們能夠將資源直接賦予當前活躍的Socket通訊的話,能夠明顯的提升資源利用率。

先附上參考資料將在後序更新。

參考書籍

HTTP權威指南
Java TCP/IP Socket 編程
Java Multithread servers
Java NIO ServerSocketChannel

clipboard.png
想要了解更多開發技術,面試教程以及互聯網公司內推,歡迎關注個人微信公衆號!將會不按期的發放福利哦~

相關文章
相關標籤/搜索