TCP/IP學習筆記一:BIO的網絡編程-簡單實例

TCP/IP學習筆記一:BIO的網絡編程-簡單實例

標籤(空格分隔): BIO 網絡編程


一、簡單的BIO網絡編程

實例:實現簡單的web服務器(簡單模仿Tomcat的請求和響應)

服務器端的實現:

    package com.bio.socket;

    import java.io.BufferedReader;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.PrintWriter;
    import java.net.InetSocketAddress;
    import java.net.ServerSocket;
    import java.net.Socket;

    /**
     * 服務器端
     * @author MOTUI
     *
     */
    public class BIOServerSocket {

        public static void main(String[] args) throws Exception {

            //1.創建serverSocket
            ServerSocket serverSocket = new ServerSocket();

            //2.設置端口號
            serverSocket.bind(new InetSocketAddress(8989));

            System.out.println("監聽端口爲8989的服務器");

            //3.獲取報文
            Socket socket = serverSocket.accept();

            //a.獲得客戶端的意圖   request
            InputStream is = socket.getInputStream();
            //字符流和字節流的轉換
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader br = new BufferedReader(isr);

            //存儲讀取的數據
            StringBuilder sb = new StringBuilder();
            String line = null;
            //讀取數據
            while((line = br.readLine()) != null){
                //將數據存儲在StringBuilder中
                sb.append(line);
            }
            System.out.println("服務器收到的數據:"+sb.toString());
            //b.返回客戶端數據
            OutputStream os = socket.getOutputStream();
            //獲得輸出流
            PrintWriter pw = new PrintWriter(os);
            pw.write("接收的數據:"+sb.toString()+"  服務器已經接收到數據");
            //將數據發送
            pw.flush();
            //關閉流
            br.close();
            pw.close();
            socket.close();
        }
    }

客戶端的實現:

    package com.bio.socket;

    import java.io.BufferedReader;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.PrintWriter;
    import java.net.InetSocketAddress;
    import java.net.Socket;

    /**
     * 客戶端
     * @author MOTUI
     *
     */
    public class BIOClientSocket {

        public static void main(String[] args) throws Exception {

            //1.創建Socket
            Socket socket = new Socket();

            //2.連接服務器
            socket.connect(new InetSocketAddress("192.168.0.117",8989));

            //a.發送數據到服務器
            OutputStream os = socket.getOutputStream();
            //獲得輸出流
            PrintWriter pw = new PrintWriter(os);
            pw.write("這是客戶端數據");
            //將數據發送
            pw.flush();
            //告知服務器已經到了流的結尾
            socket.shutdownOutput();

            //b.獲得服務器的迴應
            InputStream is = socket.getInputStream();
            //字符流和字節流的轉換
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader br = new BufferedReader(isr);

            //存儲讀取的數據
            StringBuilder sb = new StringBuilder();
            String line = null;
            //讀取數據
            while((line = br.readLine()) != null){
                //將數據存儲在StringBuilder中
                sb.append(line);
            }
            //打印服務器迴應數據
            System.out.println(sb.toString());

            //關閉流
            br.close();
            pw.close();
            socket.close();
        }
    }

運行服務器端:

    監聽端口爲8989的服務器

運行客戶端:

    接收的數據:這是客戶端數據  服務器已經接收到數據

運行客戶端之後服務器端結果爲:

    監聽端口爲8989的服務器
    服務器收到的數據:這是客戶端數據
    到此服務器端和客戶端運行結束。這樣的結果並和我們想要的結果不同,我們需要的是服務器端一直監聽客戶端發送的請求並作出響應。我們【服務器端】做如下的修改,客戶端代碼不做任何修改。

服務器端修改爲:

在執行的代碼中加上while(true)【第180行】,保證執行代碼一直執行。
package com.bio.socket;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 服務器端
 * @author MOTUI
 *
 */
public class BIOServerSocket {

    public static void main(String[] args) throws Exception {

        //1.創建serverSocket
        ServerSocket serverSocket = new ServerSocket();

        //2.設置端口號
        serverSocket.bind(new InetSocketAddress(8989));

        while(true){
            System.out.println("監聽端口爲8989的服務器");

            //3.獲取報文
            Socket socket = serverSocket.accept();

            //a.獲得客戶端的意圖   request
            InputStream is = socket.getInputStream();
            //字符流和字節流的轉換
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader br = new BufferedReader(isr);

            //存儲讀取的數據
            StringBuilder sb = new StringBuilder();
            String line = null;
            //讀取數據
            while((line = br.readLine()) != null){
                //將數據存儲在StringBuilder中
                sb.append(line);
            }
            System.out.println("服務器收到的數據:"+sb.toString());
            //b.返回客戶端數據
            OutputStream os = socket.getOutputStream();
            //獲得輸出流
            PrintWriter pw = new PrintWriter(os);
            pw.write("接收的數據:"+sb.toString()+"  服務器已經接收到數據");
            //將數據發送
            pw.flush();
            //關閉流
            br.close();
            pw.close();
            socket.close();
        }
    }
}

運行服務器端:

    監聽端口爲8989的服務器

運行客戶端:

    接收的數據:這是客戶端數據  服務器已經接收到數據

運行客戶端之後服務器端結果爲:

   監聽端口爲8989的服務器
   服務器收到的數據:這是客戶端數據
   監聽端口爲8989的服務器
    到此服務器端和客戶端已經執行結束。也達到了一直監聽客戶端的請求的目的,但是還是有問題:我們處理客戶端的請求的時候會存在一個請求沒有處理完成,另一個請求不能處理。(在處理的過程中最耗時的操作就是I/O操作)。爲了解決這個問題,我們進行如下修改。

服務器端代碼修改爲:

package com.bio.socket;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 服務器端
 * @author MOTUI
 *
 */
public class BIOServerSocket {

    public static void main(String[] args) throws Exception {

        //1.創建serverSocket
        ServerSocket serverSocket = new ServerSocket();

        //2.設置端口號
        serverSocket.bind(new InetSocketAddress(8989));

        while(true){
            System.out.println("監聽端口爲8989的服務器");

            //3.獲取報文
            final Socket socket = serverSocket.accept();

            //啓動線程
            new Thread(){
                public void run() {
                    try {
                        //a.獲得客戶端的意圖   request
                        InputStream is = socket.getInputStream();
                        //字符流和字節流的轉換
                        InputStreamReader isr = new InputStreamReader(is);
                        BufferedReader br = new BufferedReader(isr);

                        //存儲讀取的數據
                        StringBuilder sb = new StringBuilder();
                        String line = null;
                        //讀取數據
                        while((line = br.readLine()) != null){
                            //將數據存儲在StringBuilder中
                            sb.append(line);
                        }
                        System.out.println("服務器收到的數據:"+sb.toString());
                        //b.返回客戶端數據
                        OutputStream os = socket.getOutputStream();
                        //獲得輸出流
                        PrintWriter pw = new PrintWriter(os);
                        pw.write("接收的數據:"+sb.toString()+"  服務器已經接收到數據");
                        //將數據發送
                        pw.flush();
                        //關閉流
                        br.close();
                        pw.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }finally{
                        try {
                            socket.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }

                };
            }.start();
        }
    }
}

客戶端代碼修改爲:

package com.bio.socket;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;

/**
 * 客戶端
 * @author MOTUI
 *
 */
public class BIOClientSocket {

    public static void main(String[] args) throws Exception {

        //使用線程模擬用戶 併發訪問
        for (int i = 0; i < 20; i++) {
            new Thread(){
                public void run() {
                    try {
                        //1.創建Socket
                        Socket socket = new Socket();

                        //2.連接服務器
                        socket.connect(new InetSocketAddress("192.168.0.117",8989));

                        //a.發送數據到服務器
                        OutputStream os = socket.getOutputStream();
                        //獲得輸出流
                        PrintWriter pw = new PrintWriter(os);
                        pw.write("這是客戶端數據");
                        //將數據發送
                        pw.flush();
                        //告知服務器已經到了流的結尾
                        socket.shutdownOutput();

                        //b.獲得服務器的迴應
                        InputStream is = socket.getInputStream();
                        //字符流和字節流的轉換
                        InputStreamReader isr = new InputStreamReader(is);
                        BufferedReader br = new BufferedReader(isr);

                        //存儲讀取的數據
                        StringBuilder sb = new StringBuilder();
                        String line = null;
                        //讀取數據
                        while((line = br.readLine()) != null){
                            //將數據存儲在StringBuilder中
                            sb.append(line);
                        }
                        //打印服務器迴應數據
                        System.out.println(sb.toString());

                        //關閉流
                        br.close();
                        pw.close();
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                };
            }.start();
        }   
    }
}

運行服務器端:

    監聽端口爲8989的服務器

運行客戶端:

    接收的數據:這是客戶端數據  服務器已經接收到數據
    ....(省略19個:監聽端口爲8989的服務器)

運行客戶端之後服務器端結果爲:

    監聽端口爲8989的服務器
    ....(省略17個:監聽端口爲8989的服務器)
    監聽端口爲8989的服務器
    當前線程ID:9
    監聽端口爲8989的服務器
    當前線程ID:10
    服務器收到的數據:這是客戶端數據
    服務器收到的數據:這是客戶端數據
    當前線程ID:11
    當前線程ID:12
    服務器收到的數據:這是客戶端數據
    服務器收到的數據:這是客戶端數據
    當前線程ID:13
    服務器收到的數據:這是客戶端數據
    當前線程ID:14
    服務器收到的數據:這是客戶端數據
    當前線程ID:15
    當前線程ID:17
    服務器收到的數據:這是客戶端數據
    服務器收到的數據:這是客戶端數據
    當前線程ID:18
    當前線程ID:21
    當前線程ID:20
    服務器收到的數據:這是客戶端數據
    服務器收到的數據:這是客戶端數據
    服務器收到的數據:這是客戶端數據
    當前線程ID:23
    當前線程ID:22
    服務器收到的數據:這是客戶端數據
    服務器收到的數據:這是客戶端數據
    當前線程ID:24
    當前線程ID:25
    當前線程ID:26
    服務器收到的數據:這是客戶端數據
    服務器收到的數據:這是客戶端數據
    服務器收到的數據:這是客戶端數據
    當前線程ID:28
    當前線程ID:27
    服務器收到的數據:這是客戶端數據
    服務器收到的數據:這是客戶端數據
    當前線程ID:16
    當前線程ID:19
    服務器收到的數據:這是客戶端數據
    服務器收到的數據:這是客戶端數據
    到此運行結束,運行結果在不同的機器上有不同的順序這與機器的運行效率有關。分析到這裏我們感覺這種方式已經很合理了。在瞭解高併發之前,這的確看着不錯。但是我們可以思考一個問題:如果我們的併發打到萬級或者百萬級或者更高的時候,我們的程序有沒有問題呢?我們每有一個請求就創建一個線程,而且我們創建線程之後並沒有關注這個線程是否正確執行?是否執行結束?我們都沒有關注,這時候就出現問題了,當高併發情況下有很多的線程處於阻塞狀態,而我們的系統資源已經佔用,系統對I/O的處理就會慢,對I/O的處理變慢就會導致我們的線程阻塞,惡性循環直到系統假死(宕機)。這樣的處理是不是不合理呢。我們現在雖然無法做到線程不阻塞(不阻塞就是NIO的方式),但是我們可以進行其他的做法,我們可以控制線程的創建個數,這樣就會相對打到一個平衡。
    對於我們如何控制線程創建的個數呢?我們可能會想到用計數器,javaAPI中提供了線程池的方式,使用更簡單。代碼:

服務器端修改爲:

package com.bio.socket;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 服務器端
 * @author MOTUI
 *
 */
public class BIOServerSocket {

    public static void main(String[] args) throws Exception {

        //1.創建serverSocket
        ServerSocket serverSocket = new ServerSocket();

        //2.設置端口號
        serverSocket.bind(new InetSocketAddress(8989));

        //創建線程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        while(true){
            System.out.println("監聽端口爲8989的服務器");

            //3.獲取報文
            final Socket socket = serverSocket.accept();

            executorService.submit(new Runnable() {

                @Override
                public void run() {
                    try {
                        //打印當前線程ID
                        System.out.println("當前線程ID:"+Thread.currentThread().getId());

                        //a.獲得客戶端的意圖   request
                        InputStream is = socket.getInputStream();
                        //字符流和字節流的轉換
                        InputStreamReader isr = new InputStreamReader(is);
                        BufferedReader br = new BufferedReader(isr);

                        //存儲讀取的數據
                        StringBuilder sb = new StringBuilder();
                        String line = null;
                        //讀取數據
                        while((line = br.readLine()) != null){
                            //將數據存儲在StringBuilder中
                            sb.append(line);
                        }
                        System.out.println("服務器收到的數據:"+sb.toString());
                        //b.返回客戶端數據
                        OutputStream os = socket.getOutputStream();
                        //獲得輸出流
                        PrintWriter pw = new PrintWriter(os);
                        pw.write("接收的數據:"+sb.toString()+"  服務器已經接收到數據");
                        //將數據發送
                        pw.flush();
                        //關閉流
                        br.close();
                        pw.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }finally{
                        try {
                            socket.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }       
                }
            });
        }
    }
}

運行服務器端:

    監聽端口爲8989的服務器

運行客戶端:

    接收的數據:這是客戶端數據  服務器已經接收到數據
    ....(省略19個:監聽端口爲8989的服務器)

運行客戶端之後服務器端結果爲:

    監聽端口爲8989的服務器
    監聽端口爲8989的服務器
    監聽端口爲8989的服務器
    監聽端口爲8989的服務器
    當前線程ID:9
    監聽端口爲8989的服務器
    當前線程ID:10
    監聽端口爲8989的服務器
    監聽端口爲8989的服務器
    當前線程ID:12
    服務器收到的數據:這是客戶端數據
    服務器收到的數據:這是客戶端數據
    當前線程ID:13
    服務器收到的數據:這是客戶端數據
    監聽端口爲8989的服務器
    當前線程ID:11
    監聽端口爲8989的服務器
    服務器收到的數據:這是客戶端數據
    服務器收到的數據:這是客戶端數據
    當前線程ID:14
    監聽端口爲8989的服務器
    監聽端口爲8989的服務器
    服務器收到的數據:這是客戶端數據
    監聽端口爲8989的服務器
    監聽端口爲8989的服務器
    監聽端口爲8989的服務器
    監聽端口爲8989的服務器
    監聽端口爲8989的服務器
    監聽端口爲8989的服務器
    監聽端口爲8989的服務器
    監聽端口爲8989的服務器
    監聽端口爲8989的服務器
    監聽端口爲8989的服務器
    當前線程ID:14
    服務器收到的數據:這是客戶端數據
    當前線程ID:9
    當前線程ID:13
    當前線程ID:14
    服務器收到的數據:這是客戶端數據
    當前線程ID:10
    當前線程ID:12
    服務器收到的數據:這是客戶端數據
    當前線程ID:9
    服務器收到的數據:這是客戶端數據
    服務器收到的數據:這是客戶端數據
    服務器收到的數據:這是客戶端數據
    服務器收到的數據:這是客戶端數據
    當前線程ID:12
    當前線程ID:10
    服務器收到的數據:這是客戶端數據
    當前線程ID:9
    服務器收到的數據:這是客戶端數據
    服務器收到的數據:這是客戶端數據
    當前線程ID:15
    當前線程ID:17
    服務器收到的數據:這是客戶端數據
    服務器收到的數據:這是客戶端數據
    當前線程ID:18
    當前線程ID:16
    服務器收到的數據:這是客戶端數據
    服務器收到的數據:這是客戶端數據
    到此我們的程序就算相對完善的。在服務器端我們設置了線程的最大數爲10,所以我們的程序最大併發量爲10,這樣我們的程序就不需要一直新建線程浪費資源,只需要等待別人用完還回線程池,然後拿到繼續使用即可。但是我們的程序依然是線程阻塞的。

總結:

    對於BIO的網絡編程,我們實現的是傳輸層的TCP/IP協議,這樣的傳輸效率更高。對於webService的SOP協議是Http+XML,過度的封裝使得效率上不適合對速度要求較高的應用。
    多線程(線程池)實現BIO並非訪問,將IO的處理交給線程池裏的線程,限定系統線程無止境的創建節省系統資源實現線程複用.
    缺點: 線程會因爲IO沒有就緒掛起.

問題分析:

    上述程序屬於僞異步I/O,存在什麼樣的弊端?

/**
* Reads some number of bytes from the input stream and stores them into
* the buffer array b. The number of bytes actually read is
* returned as an integer. **This method blocks until input data is
* available, end of file is detected, or an exception is thrown.**
*
*

If the length of b is zero, then no bytes are read and
* 0 is returned; otherwise, there is an attempt to read at
* least one byte. If no byte is available because the stream is at the
* end of the file, the value -1 is returned; otherwise, at
* least one byte is read and stored into b.
* ……..
*/
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}

上文中的粗體字對Socket的讀操作的說明,當Socket的輸入流進行讀操作的時候,它會一致阻塞下去,知道發生三件事:

  1. 有數據可讀
  2. 可用數據已經讀完
  3. 發生空指針或者I/O異常

這樣就意味着當發送方請求說這響應消息較慢,或者網絡傳輸較慢,讀取輸入流的一方的通信線程將會阻塞很長時間,如果對方60s發送完數據,讀入一方的通信線程將阻塞60s,在此期間其他接入消息將在消息隊列中排隊.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章