Java Socket編程 一個BIO Socket客戶端的進化

最近看了Java的IO包源碼,對BIO有了較深刻的理解。Socket編程其實也是基於IO流操做,而且其流操做都是阻塞的,就想着寫一個Socket程序並對其一步一步優化,來加深對IO的理解。本文主要從簡單的Socket鏈接開始,一步一步優化,最後使用線程池等技術提升併發。Socket源碼本篇未涉及,等有時間我再研究一番。java

一. 基本概念

Socket編程的基本流程以下圖(圖片來自網絡),一個IP地址和一個端口號稱爲一個套接字(socket)。 spring

Socket
Socket編程是BIO的,對於服務端,accept()、read()、write()都會堵塞。

  • accept是阻塞的,只有新鏈接來了,accept纔會返回,主線程才能繼續
  • read是阻塞的,只有請求消息來了,read才能返回,子線程才能繼續處理
  • write是阻塞的,只有客戶端把消息收了,write才能返回,子線程才能繼續讀取下一個請求 Socket開發Java提供了兩個類,Socket用於BIO鏈接和信息收發,ServerSocket用於構建一個服務端,其accept()方法得到一個Socket對象,最終客戶端服務器都是使用Socket進行通訊。

二. 最基本的Socket

以下,最基本的客戶端發送消息,服務端接收消息輸入。須要注意的是,因爲中文的utf8編碼是3個字節,若是使用buffer來分段接收字節流,可能致使亂碼。另外,read()是堵塞的,若是不判斷read() == -1來表示結束,那麼read()方法會一直堵塞。編程

package me.zebin.demo.javaio;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

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

@RunWith(SpringRunner.class)
@SpringBootTest
public class JavaioApplicationTests {

    @Test
    public void server() throws Exception {

        // 指定端口
        ServerSocket ss = new ServerSocket(9999);
        System.out.println("server starting...");
        // 等待鏈接
        Socket s = ss.accept();

        // 獲取輸入流,接收客戶端的消息
        InputStream is = s.getInputStream();

        // 緩存buffer,utf8編碼中文是3個字節,這裏也但是使用BufferedReader解碼
        byte[] buffer = new byte[5];
        while(true){
            int cnt = is.read(buffer);
            // 若是不判斷流結束,上面的read()讀不到數據會一直堵塞
            if(cnt == -1){
                break;
            }
            String str = new String(buffer, 0, cnt, "utf8");
            System.out.println(str);

        }
        s.close();
        ss.close();

    }

    @Test
    public void client() throws Exception{

        // 指定端口
        Socket s = new Socket("127.0.0.1", 9999);

        // 獲取輸出流,向服務端發消息
        OutputStream os = s.getOutputStream();

        // 發送消息,utf8編碼中文是3個字節,服務端使用buffer可能致使亂碼
        String str = "我是客戶端";
        os.write(str.getBytes("utf8"));
        s.close();
    }
}
複製代碼

以上程序,若是buffer設置爲5,運行結果以下,出現亂碼。 數組

亂碼
固然,解決方案能夠整行讀取,將InputStream轉爲Reader再轉BufferedReader便可讀取一行。也可以使用Scanner來解決,服務端代碼改成以下:

@Test
    public void server() throws Exception {

        // 指定端口
        ServerSocket ss = new ServerSocket(9999);
        System.out.println("server starting...");
        // 等待鏈接
        Socket s = ss.accept();

        // 獲取輸入流,接收客戶端的消息
        InputStream is = s.getInputStream();

        // 輸入字節流封裝爲Scanner,讀取整行
        Scanner sc = new Scanner(is, "utf8");
        while (sc.hasNextLine()){
            System.out.println(sc.nextLine());
        }

        s.close();
        ss.close();

    }
複製代碼

運行結果以下,沒有亂碼了。 緩存

Scanner
服務端判斷流關閉,通常使用兩種方法。

  1. 使用特殊符號:既然上面能夠獲取到行,服務端客戶端就能夠約定相關的結束符,如接收到一個空行就結束,服務端進行判斷關閉流便可。
  2. 使用長度界定:相似http協議就有content-length界定結束符,咱們也能夠在客戶端發送byte[]數組前,在byte[]數據前兩個字節標識消息長度。固然,兩個字節能表示的消息長度就只有2^16-1,即大小是2^16字節,即64k大小。

三. 多線程版本

上面的版本有一個弊端,就是一個服務器只能提供給一個客戶端進行鏈接,若是將鏈接的用線程處理,服務器能夠處理更多的客戶端鏈接,代碼以下:服務器

@Test
    public void server() throws Exception {

        // 指定端口
        ServerSocket ss = new ServerSocket(9998);
        System.out.println("server starting...");
        while(true){
            // 等待鏈接
            Socket s = ss.accept();
            System.out.println("得到鏈接");
            Thread t = new Thread(new ServerThread(s));
            t.start();
        }
    }

    class ServerThread implements Runnable{

        private Socket s;

        ServerThread(Socket s){
            this.s = s;
        }

        @Override
        public void run(){
            // 獲取輸入流,接收客戶端的消息
            InputStream is = null;
            try {
                is = s.getInputStream();
                // 使用Scanner封裝
                Scanner sc = new Scanner(is, "utf8");
                while (sc.hasNextLine()){
                    System.out.println(sc.nextLine());
                }
                s.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
複製代碼

四. 線程池版本

以上多線程版本咱們使用了多線程來處理併發,不過線程的建立和銷燬都會消耗大量的資源和時間,同時,高併發下會建立很是多的線程,且不說操做系統能開啓的線程數有限,操做系統維護和切換大量的線程也會很是耗時。因此使用線程池,只用4個線程,用隊列將未執行到的線程排隊處理,減小了線程數量,同時也避免了建立和銷燬線程帶來的性能問題。網絡

@Test
    public void server() throws Exception {

        // 指定端口
        ServerSocket ss = new ServerSocket(9998);
        System.out.println("server starting...");

        // 建立線程隊列
        BlockingQueue bq = new ArrayBlockingQueue(100);
        // 拒絕策略
        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
        Executor executor = new ThreadPoolExecutor(4, 8, 1, TimeUnit.MINUTES, bq, handler);
        while(true){
            // 等待鏈接
            Socket s = ss.accept();
            System.out.println("得到鏈接");
            Thread t = new Thread(new ServerThread(s));
            executor.execute(t);
        }
    }
複製代碼

以上,本篇結束。多線程

參考資料

相關文章
相關標籤/搜索