通過(guò)socket編程掌握IO流 —— BIO

一、本次目標(biāo)

  1. 編寫(xiě)最簡(jiǎn)單的1:1的server:client,感受socket通信編程;
  2. 修改client為多線程,模擬多個(gè)client請(qǐng)求server,觀測(cè)server響應(yīng);
  3. 封裝server處理client請(qǐng)求方法,另開(kāi)線程處理;
  4. 處理線程加入線程池管理,減輕server負(fù)荷;

二、動(dòng)手實(shí)踐

1、編寫(xiě)配置接口,包含默認(rèn)服務(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、編寫(xiě)時(shí)間工具類(lèi)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、編寫(xiě)服務(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)啟動(dòng)");
    try {
      while (true) {
        System.out.println("[" + DateUtil.getCurTimeStr() + "]:server循環(huán)獲取client請(qǐng)求開(kāi)始");
        // 阻塞,直到有客戶(hù)端發(fā)起請(qǐng)求
        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流會(huì)阻塞,所以這里的readLine()時(shí)間會(huì)根據(jù)客戶(hù)端操作的時(shí)間而定
          while ((line = reader.readLine()) != null) {
            builder.append(line);
            builder.append(System.getProperty("line.separator"));
          }
          System.out.println("[" + DateUtil.getCurTimeStr() + "]:client傳來(lái)消息");
          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、編寫(xiě)客戶(hù)端代碼

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寫(xiě)入消息" + msg);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }


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

5、測(cè)試IO流的阻塞性

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

首先啟動(dòng)ServerBIO,查看控制臺(tái):

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

根據(jù)server的代碼,可以發(fā)現(xiàn)目前就阻塞在server.accept()這個(gè)位置,然后我們?cè)賳?dòng)client的main方法,模擬了兩個(gè)client一前一后發(fā)送請(qǐng)求,觀察client的控制臺(tái):

[17:06:57]:連接server成功
[17:07:00]:client寫(xiě)入消息我是第一個(gè)客戶(hù)端

同時(shí)切到server的控制臺(tái):

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

[17:07:00]:server循環(huán)獲取client請(qǐng)求開(kāi)始

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

這只是一個(gè)最最簡(jiǎn)單的socket通信模型,只有一個(gè)client請(qǐng)求,那么我們修改client實(shí)現(xiàn)Runnable接口,開(kāi)啟線程發(fā)送數(shù)據(jù),模擬多個(gè)client并行訪問(wèn)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寫(xiě)入消息" + msg);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

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

運(yùn)行client的main方法,觀察client的控制臺(tái):

[17:21:44]:連接server成功
[17:21:44]:連接server成功
[17:21:45]:client寫(xiě)入消息我是第二個(gè)客戶(hù)端
[17:21:47]:client寫(xiě)入消息我是第一個(gè)客戶(hù)端

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

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

[17:21:47]:server循環(huán)獲取client請(qǐng)求開(kāi)始
[17:21:47]:有新的client連接啦
[17:21:47]:client傳來(lái)消息
我是第二個(gè)客戶(hù)端

[17:21:47]:server循環(huán)獲取client請(qǐng)求開(kāi)始

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

三、補(bǔ)充完善

1、封裝server處理client請(qǐng)求,單獨(dú)新開(kāi)線程:

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流會(huì)阻塞,所以這里的readLine()時(shí)間會(huì)根據(jù)客戶(hù)端操作的時(shí)間而定
      while ((line = reader.readLine()) != null) {
        builder.append(line);
        builder.append(System.getProperty("line.separator"));
      }
      System.out.println("[" + DateUtil.getCurTimeStr() + "]:client傳來(lái)消息");
      System.out.println(builder.toString());
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

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

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

不需要修改client代碼,直接啟動(dòng)server后,在啟動(dòng)client的main方法,觀測(cè)這次server的控制臺(tái):

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

[17:38:04]:client傳來(lái)消息
我是第一個(gè)客戶(hù)端

對(duì)比上面,很明顯server新開(kāi)了client處理線程,在處理client消息方面不會(huì)造成阻塞了。server收到client連接后新開(kāi)線程處理并立即輪詢(xún)接下來(lái)連接的client,“我是第二個(gè)客戶(hù)端”較先打印很好的印證了這點(diǎn)。

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

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

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

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

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

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

··· ···
// 在寫(xiě)之前必須關(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寫(xiě)入消息");
      System.out.println(msg);

      // 在寫(xiě)之前必須關(guān)閉寫(xiě)
      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傳來(lái)消息");
      System.out.println(builder.toString());
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

同樣的先運(yùn)行server后,再執(zhí)行client的main方法,觀測(cè)client的控制臺(tái):

[18:08:07]:連接server成功
[18:08:07]:連接server成功
[18:08:08]:client寫(xiě)入消息
我是第二個(gè)客戶(hù)端
[18:08:08]:server傳來(lái)消息
我是第二個(gè)客戶(hù)端

Disconnected from the target VM, address: '127.0.0.1:61618', transport: 'socket'
[18:08:10]:client寫(xiě)入消息
我是第一個(gè)客戶(hù)端
[18:08:10]:server傳來(lái)消息
我是第一個(gè)客戶(hù)端

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

  1. jdk 1.7開(kāi)始,try后面可直接初始化某些流(implements AutoCloseable),則會(huì)自動(dòng)關(guān)閉;
  2. socket關(guān)閉后對(duì)應(yīng)的輸入/輸出流也會(huì)被關(guān)閉;
  3. socket 一次I/O操作既有讀也有寫(xiě)的話,中間一定要加上shutup流;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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