通過socket編程掌握IO流 —— BIO

一、本次目標

  1. 編寫最簡單的1:1的server:client,感受socket通信編程;
  2. 修改client為多線程,模擬多個client請求server,觀測server響應(yīng);
  3. 封裝server處理client請求方法,另開線程處理;
  4. 處理線程加入線程池管理,減輕server負荷;

二、動手實踐

1、編寫配置接口,包含默認服務(wù)端地址、端口等

package com.cjt.io;

public interface Config {

  String DEFAULT_ENCODE = "UTF-8";

  String DEFAULT_ADDR = "127.0.0.1";

  int DEFAULT_PORT = 6666;
}

2、編寫時間工具類DateUtil,方便輸出日志打印

package com.cjt.io;

import java.text.SimpleDateFormat;
import java.util.Date;

public class DateUtil {

  private final static String DEFAULT_PATTERN = "HH:mm:ss";

  public static String getCurTimeStr(){
    SimpleDateFormat sdf = new SimpleDateFormat(DEFAULT_PATTERN);
    return sdf.format(new Date());
  }
}

3、編寫服務(wù)端代碼

package com.cjt.io.bio;

import com.cjt.io.Config;
import com.cjt.io.DateUtil;

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

public class ServerBIO implements Config{

  private ServerSocket server;

  private ServerBIO(int port){
    try {
      server = new ServerSocket(port);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }


  private void start() {
    System.out.println("[" + DateUtil.getCurTimeStr() + "]:服務(wù)端已經(jīng)啟動");
    try {
      while (true) {
        System.out.println("[" + DateUtil.getCurTimeStr() + "]:server循環(huán)獲取client請求開始");
        // 阻塞,直到有客戶端發(fā)起請求
        Socket socket = server.accept();
        System.out.println("[" + DateUtil.getCurTimeStr() + "]:有新的client連接啦");
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), DEFAULT_ENCODE))){
          String line;
          StringBuilder builder = new StringBuilder();
          // 普通IO流會阻塞,所以這里的readLine()時間會根據(jù)客戶端操作的時間而定
          while ((line = reader.readLine()) != null) {
            builder.append(line);
            builder.append(System.getProperty("line.separator"));
          }
          System.out.println("[" + DateUtil.getCurTimeStr() + "]:client傳來消息");
          System.out.println(builder.toString());
        } catch (IOException e) {
          e.printStackTrace();
          break;
        }
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  public static void main(String[] args){
    new ServerBIO(DEFAULT_PORT).start();
  }
}

4、編寫客戶端代碼

package com.cjt.io.bio;

import com.cjt.io.Config;
import com.cjt.io.DateUtil;

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

public class ClientBIO implements Config {

  private String serverIp;

  private int port;

  private String msg;

  private int second;

  private ClientBIO(String serverIp, int port) {
    this.serverIp = serverIp;
    this.port = port;
  }

  private ClientBIO second(int second) {
    this.second = second;
    return this;
  }

  private ClientBIO msg(String msg) {
    this.msg = msg;
    return this;
  }

  private void sleep(int second) {
    try {
      Thread.sleep(second * 1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  public void send() {
    try (Socket socket = new Socket(serverIp, port)) {
      System.out.println("[" + DateUtil.getCurTimeStr() + "]:連接server成功");
      BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), DEFAULT_ENCODE));
      writer.write(msg);
      writer.flush();
      sleep(second);
      System.out.println("[" + DateUtil.getCurTimeStr() + "]:client寫入消息" + msg);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }


  public static void main(String[] args) {
    ClientBIO client_1 = new ClientBIO(DEFAULT_ADDR, DEFAULT_PORT).second(3).msg("我是第一個客戶端");
    client_1.send();
  }
}

5、測試IO流的阻塞性

基于最簡單的單線程方式我們分別實現(xiàn)了server和client的編寫,這里不會詳細闡述怎么讀流,寫數(shù)據(jù),buffered流怎么用,主要是研究IO流的特性,更好地學會怎么處理高并發(fā)中的流處理。

首先啟動ServerBIO,查看控制臺:

[17:06:54]:服務(wù)端已經(jīng)啟動
[17:06:54]:server循環(huán)獲取client請求開始

根據(jù)server的代碼,可以發(fā)現(xiàn)目前就阻塞在server.accept()這個位置,然后我們再啟動client的main方法,模擬了兩個client一前一后發(fā)送請求,觀察client的控制臺:

[17:06:57]:連接server成功
[17:07:00]:client寫入消息我是第一個客戶端

同時切到server的控制臺:

[17:06:54]:服務(wù)端已經(jīng)啟動
[17:06:54]:server循環(huán)獲取client請求開始
[17:06:57]:有新的client連接啦
[17:07:00]:client傳來消息
我是第一個客戶端

[17:07:00]:server循環(huán)獲取client請求開始

57秒client連接server成功,同時server打印“有新的client連接”,由于手動sleep的原因,直到00秒時client請求寫入數(shù)據(jù)才完成,而server便一直阻塞在reader.readLine()這個位置,直接阻塞到00秒讀取到client的消息,然后繼續(xù)循環(huán)獲取client阻塞在server.accept()這個位置。

這只是一個最最簡單的socket通信模型,只有一個client請求,那么我們修改client實現(xiàn)Runnable接口,開啟線程發(fā)送數(shù)據(jù),模擬多個client并行訪問server:

package com.cjt.io.bio;

import com.cjt.io.Config;
import com.cjt.io.DateUtil;

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

public class ClientBIO implements Config, Runnable {

  private String serverIp;

  private int port;

  private String msg;

  private int second;

  private ClientBIO(String serverIp, int port) {
    this.serverIp = serverIp;
    this.port = port;
  }

  private ClientBIO second(int second) {
    this.second = second;
    return this;
  }

  private ClientBIO msg(String msg) {
    this.msg = msg;
    return this;
  }

  private void sleep(int second) {
    try {
      Thread.sleep(second * 1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  @Override
  public void run() {
    try (Socket socket = new Socket(serverIp, port)) {
      System.out.println("[" + DateUtil.getCurTimeStr() + "]:連接server成功");
      BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), DEFAULT_ENCODE));
      writer.write(msg);
      writer.flush();
      sleep(second);
      System.out.println("[" + DateUtil.getCurTimeStr() + "]:client寫入消息" + msg);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  public static void main(String[] args) {
    ClientBIO client_1 = new ClientBIO(DEFAULT_ADDR, DEFAULT_PORT).second(3).msg("我是第一個客戶端");
    new Thread(client_1).start();
    ClientBIO client_2 = new ClientBIO(DEFAULT_ADDR, DEFAULT_PORT).second(1).msg("我是第二個客戶端");
    new Thread(client_2).start();
  }
}

運行client的main方法,觀察client的控制臺:

[17:21:44]:連接server成功
[17:21:44]:連接server成功
[17:21:45]:client寫入消息我是第二個客戶端
[17:21:47]:client寫入消息我是第一個客戶端

很明顯多線程并行發(fā)送請求到server,由于client_2的sleep為1秒的原因,較client_1先打印,然后回到server的控制臺:

[17:21:37]:服務(wù)端已經(jīng)啟動
[17:21:37]:server循環(huán)獲取client請求開始
[17:21:44]:有新的client連接啦
[17:21:47]:client傳來消息
我是第一個客戶端

[17:21:47]:server循環(huán)獲取client請求開始
[17:21:47]:有新的client連接啦
[17:21:47]:client傳來消息
我是第二個客戶端

[17:21:47]:server循環(huán)獲取client請求開始

server這里控制臺輸出完全相反,44秒提示“有新的client連接啦”,這里是client_1,通過后面的msg也可以知道,然后在47秒讀取完client_1后立即獲取到client_2的連接,雖然client_2的sleep有1秒的時間,但是由于client_2的連接時間在44秒,所以數(shù)據(jù)早已準備就緒,但是server的線程阻塞在讀取client_1了。

三、補充完善

1、封裝server處理client請求,單獨新開線程:

package com.cjt.io.bio;

import com.cjt.io.Config;
import com.cjt.io.DateUtil;

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

public class ClientHandler implements Runnable, Config {

  private Socket socket;

  ClientHandler(Socket socket) {
    this.socket = socket;
  }

  @Override
  public void run() {
    try {
      BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), DEFAULT_ENCODE));
      String line;
      StringBuilder builder = new StringBuilder();
      // 普通IO流會阻塞,所以這里的readLine()時間會根據(jù)客戶端操作的時間而定
      while ((line = reader.readLine()) != null) {
        builder.append(line);
        builder.append(System.getProperty("line.separator"));
      }
      System.out.println("[" + DateUtil.getCurTimeStr() + "]:client傳來消息");
      System.out.println(builder.toString());
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

同時修改server的start調(diào)用代碼:

System.out.println("[" + DateUtil.getCurTimeStr() + "]:服務(wù)端已經(jīng)啟動");
while (true) {
  try {
    System.out.println("[" + DateUtil.getCurTimeStr() + "]:server循環(huán)獲取client請求開始");
    // 阻塞,直到有客戶端發(fā)起請求
    Socket socket = server.accept();
    System.out.println("[" + DateUtil.getCurTimeStr() + "]:有新的client連接啦");
    new Thread(new ClientHandler(socket)).start();
  } catch (IOException e) {
    e.printStackTrace();
    break;
  }
}

不需要修改client代碼,直接啟動server后,在啟動client的main方法,觀測這次server的控制臺:

[17:37:53]:服務(wù)端已經(jīng)啟動
[17:37:53]:server循環(huán)獲取client請求開始
[17:38:00]:有新的client連接啦
[17:38:00]:server循環(huán)獲取client請求開始
[17:38:00]:有新的client連接啦
[17:38:00]:server循環(huán)獲取client請求開始
[17:38:02]:client傳來消息
我是第二個客戶端

[17:38:04]:client傳來消息
我是第一個客戶端

對比上面,很明顯server新開了client處理線程,在處理client消息方面不會造成阻塞了。server收到client連接后新開線程處理并立即輪詢接下來連接的client,“我是第二個客戶端”較先打印很好的印證了這點。

2、將ClientHandler處理線程加入到線程池

Java創(chuàng)建的線程屬于寶貴的系統(tǒng)資源,若是在大量的client訪問時,則會創(chuàng)建大量的client消息處理線程,這樣必將導(dǎo)致系統(tǒng)性能急劇下降,最終宕掉。所以我們可以將client消息處理線程加入到線程池中進行管理:

··· ···
// 定長線程池,超出數(shù)量的線程會在隊列中等待
private ExecutorService threadPool = Executors.newFixedThreadPool(20);
··· ···
// 創(chuàng)建一個新的線程加入到線程池中管理并立即提交執(zhí)行
threadPool.execute(new Thread(new ClientHandler(socket)));
··· ···

這樣就不用擔心由于高并發(fā)產(chǎn)生巨大的線程從而使系統(tǒng)崩潰,運行結(jié)果與上面相差無異。這里就不再重復(fù),感興趣的可以自己試試。

3、增加server響應(yīng)

修改ClientHandler,簡單回應(yīng)client(復(fù)述消息):

··· ···
// 在寫之前必須關(guān)閉讀
socket.shutdownInput();

BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), DEFAULT_ENCODE));
writer.write(builder.toString());
writer.flush();

socket.close();
··· ···

那么client的發(fā)送消息也得需要讀取server的響應(yīng)了,修改client的run方法:

  @Override
  public void run() {
    try (Socket socket = new Socket(serverIp, port)) {
      System.out.println("[" + DateUtil.getCurTimeStr() + "]:連接server成功");
      BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), DEFAULT_ENCODE));
      writer.write(msg);
      writer.flush();
      sleep(second);
      System.out.println("[" + DateUtil.getCurTimeStr() + "]:client寫入消息");
      System.out.println(msg);

      // 在寫之前必須關(guān)閉寫
      socket.shutdownOutput();

      BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), DEFAULT_ENCODE));
      StringBuilder builder = new StringBuilder();
      while ((msg = reader.readLine()) != null){
        builder.append(msg);
        builder.append(System.getProperty("line.separator"));
      }
      System.out.println("[" + DateUtil.getCurTimeStr() + "]:server傳來消息");
      System.out.println(builder.toString());
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

同樣的先運行server后,再執(zhí)行client的main方法,觀測client的控制臺:

[18:08:07]:連接server成功
[18:08:07]:連接server成功
[18:08:08]:client寫入消息
我是第二個客戶端
[18:08:08]:server傳來消息
我是第二個客戶端

Disconnected from the target VM, address: '127.0.0.1:61618', transport: 'socket'
[18:08:10]:client寫入消息
我是第一個客戶端
[18:08:10]:server傳來消息
我是第一個客戶端

這說明已經(jīng)server成功做出了響應(yīng),并且client也成功讀取到了server的消息。這里需要強調(diào)的是:

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

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

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,781評論 18 139
  • 一、本次目標 改造server,采用NIO讀取client信息; 改造client,亦采用NIO發(fā)送消息,與之前不...
    叫我宮城大人閱讀 1,228評論 0 0
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,614評論 25 708
  • 那個夏夜,燥熱里夾雜著一點清涼味。 白云瘋玩了一天,突然心血來潮的涂了個烏黑烏黑的臉,于田野的上空肆意橫行。風丫頭...
    淺秋遺夢閱讀 343評論 1 0
  • 青云直上三千尺, 靈霞吐虹萬里香。 無量云帆聽松瀑, 獨霸朝陽任飛翔。 猶見星辰欲暗渡, 不負韶光自激蕩。
    曦微w行走在路上閱讀 801評論 0 7