基于BIO的Java Socket通信详解

BIO,即阻塞IO,在基于Socket的消息通信过程中,Socket服务端向外部提供服务,而Socket客户端可以建立到Socket服务端的连接,进而发送请求数据,然后等待Socket服务端处理,并返回处理结果(响应)。
基于BIO的通信,Socket服务端会发生阻塞,即在监听过程中每次accept到一个客户端的Socket连接,就要处理这个请求,而此时其他连接过来的客户端只能阻塞等待。可见,这种模式下Socket服务端的处理能力是非常有限的,客户端也只能等待,直到服务端空闲时进行请求的处理。

BIO通信实现

下面基于BIO模式,来实现一个简单的Socket服务端与Socket客户端进行通信的逻辑,对这种通信方式有一个感性的认识。具体逻辑描述如下:
1、Socket客户端连接到Socket服务端,并发送数据“I am the client N.”;
2、Socket服务端,监听服务端口,并接收客户端请求数据,如果请求数据以“I am the client”开头,则响应客户端“I am the server, and you are the Nth client.”;

Socket服务端实现,代码如下所示:

package org.shirdrn.java.communications.bio; 

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

/**
 * 基于BIO的Socket服务器端
 *
 * @author shirdrn
 */
public class SimpleBioTcpServer extends Thread { 

  /** 服务端口号 */
  private int port = 8888;
  /** 为客户端分配编号 */
  private static int sequence = 0; 

  public SimpleBioTcpServer(int port) {
    this.port = port;
  } 

  @Override
  public void run() {
    Socket socket = null;
    try {
      ServerSocket serverSocket = new ServerSocket(this.port);
      while(true) {
        socket = serverSocket.accept(); // 监听
        this.handleMessage(socket); // 处理一个连接过来的客户端请求
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  } 

  /**
   * 处理一个客户端socket连接
   * @param socket 客户端socket
   * @throws IOException
   */
  private void handleMessage(Socket socket) throws IOException {
    InputStream in = socket.getInputStream(); // 流:客户端->服务端(读)
    OutputStream out = socket.getOutputStream(); // 流:服务端->客户端(写)
    int receiveBytes;
    byte[] receiveBuffer = new byte[128];
    String clientMessage = "";
    if((receiveBytes=in.read(receiveBuffer))!=-1) {
      clientMessage = new String(receiveBuffer, 0, receiveBytes);
      if(clientMessage.startsWith("I am the client")) {
        String serverResponseWords =
          "I am the server, and you are the " + (++sequence) + "th client.";
        out.write(serverResponseWords.getBytes());
      }
    }
    out.flush();
    System.out.println("Server: receives clientMessage->" + clientMessage);
  } 

  public static void main(String[] args) {
    SimpleBioTcpServer server = new SimpleBioTcpServer(1983);
    server.start();
  }
} 

上述实现,没有进行复杂的异常处理。

Socket客户端实现,代码如下所示:

package org.shirdrn.java.communications.bio; 

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Date; 

/**
 * 基于BIO的Socket客户端
 *
 * @author shirdrn
 */
public class SimpleBioTcpClient { 

  private String ipAddress;
  private int port;
  private static int pos = 0; 

  public SimpleBioTcpClient() {} 

  public SimpleBioTcpClient(String ipAddress, int port) {
    this.ipAddress = ipAddress;
    this.port = port;
  } 

  /**
   * 连接Socket服务端,并模拟发送请求数据
   * @param data 请求数据
   */
  public void send(byte[] data) {
    Socket socket = null;
    OutputStream out = null;
    InputStream in = null;
    try {
      socket = new Socket(this.ipAddress, this.port); // 连接
      // 发送请求
      out = socket.getOutputStream();
      out.write(data);
      out.flush();
      // 接收响应
      in = socket.getInputStream();
      int totalBytes = 0;
      int receiveBytes = 0;
      byte[] receiveBuffer = new byte[128];
      if((receiveBytes=in.read(receiveBuffer))!=-1) {
        totalBytes += receiveBytes;
      }
      String serverMessage = new String(receiveBuffer, 0, receiveBytes);
      System.out.println("Client: receives serverMessage->" + serverMessage);
    } catch (UnknownHostException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      try {
        // 发送请求并接收到响应,通信完成,关闭连接
        out.close();
        in.close();
        socket.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  } 

  public static void main(String[] args) {
    int n = 1;
    StringBuffer data = new StringBuffer();
    Date start = new Date();
    for(int i=0; i<n; i++) {
      data.delete(0, data.length());
      data.append("I am the client ").append(++pos).append(".");
      SimpleBioTcpClient client = new SimpleBioTcpClient("localhost", 1983);
      client.send(data.toString().getBytes());
    }
    Date end = new Date();
    long cost = end.getTime() - start.getTime();
    System.out.println(n + " requests cost " + cost + " ms.");
  }
} 

首先启动Socket服务端进程SimpleBioTcpServer,然后再运行Socket客户端SimpleBioTcpClient。可以看到,服务端接收到请求数据,然后响应客户端,客户端接收到了服务端的响应数据。

上述实现中,对于Socket客户端和服务端都是一次写入,并一次读出,而在实际中如果每次通信过程中数据量特别大的话,服务器端是不可能接受的,可以在确定客户端请求数据字节数的情况,循环来读取并进行处理。

另外,对于上述实现中流没有进行装饰(Wrapped)处理,在实际中会有性能的损失,如不能缓冲等。

对于Socket服务端接收数据,如果可以使多次循环读取到的字节数据通过一个可变长的字节缓冲区来存储,就能方便多了,可是使用ByteArrayOutputStream,例如:

ByteArrayOutputStream data = new ByteArrayOutputStream();
data.write(receiveBuffer, totalBytes , totalBytes + receiveBytes);

BIO通信测试

下面测试一下大量请求的场景下,Socket服务端处理的效率。

第一种方式:通过for循环来启动5000个Socket客户端,发送请求,代码如下所示:

public static void main(String[] args) {
  int n = 5000;
  StringBuffer data = new StringBuffer();
  Date start = new Date();
  for(int i=0; i<n; i++) {
    data.delete(0, data.length());
    data.append("I am the client ").append(++pos).append(".");
    SimpleBioTcpClient client = new SimpleBioTcpClient("localhost", 1983);
    client.send(data.toString().getBytes());
  }
  Date end = new Date();
  long cost = end.getTime() - start.getTime();
  System.out.println(n + " requests cost " + cost + " ms.");
} 

经过测试,大约需要9864ms,大概接近10s。

第二种方式:通过启动5000个独立的客户端线程,同时请求,服务端进行计数:

package org.shirdrn.java.communications.bio; 

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Date; 

/**
 * 基于BIO的Socket通信测试
 *
 * @author shirdrn
 */
public class SimpleBioTcpTest { 

  static int threadCount = 5000; 

  /**
   * 基于BIO的Socket服务端进程
   *
   * @author shirdrn
   */
  static class SocketServer extends Thread { 

    /** 服务端口号 */
    private int port = 8888;
    /** 为客户端分配编号 */
    private static int sequence = 0; 

    public SocketServer(int port) {
      this.port = port;
    } 

    @Override
    public void run() {
      Socket socket = null;
      int counter = 0;
      try {
        ServerSocket serverSocket = new ServerSocket(this.port);
        boolean flag = false;
        Date start = null;
        while(true) {
          socket = serverSocket.accept(); // 监听
          // 有请求到来才开始计时
          if(!flag) {
            start = new Date();
            flag = true;
          }
          this.handleMessage(socket); // 处理一个连接过来的客户端请求
          if(++counter==threadCount) {
            Date end = new Date();
            long last = end.getTime() - start.getTime();
            System.out.println(threadCount + " requests cost " + last + " ms.");
          }
        }
      } catch (IOException e) {
        e.printStackTrace();
      }
    } 

    /**
     * 处理一个客户端socket连接
     * @param socket 客户端socket
     * @throws IOException
     */
    private void handleMessage(Socket socket) throws IOException {
      InputStream in = socket.getInputStream(); // 流:客户端->服务端(读)
      OutputStream out = socket.getOutputStream(); // 流:服务端->客户端(写)
      int receiveBytes;
      byte[] receiveBuffer = new byte[128];
      String clientMessage = "";
      if((receiveBytes=in.read(receiveBuffer))!=-1) {
        clientMessage = new String(receiveBuffer, 0, receiveBytes);
        if(clientMessage.startsWith("I am the client")) {
          String serverResponseWords =
            "I am the server, and you are the " + (++sequence) + "th client.";
          out.write(serverResponseWords.getBytes());
        }
      }
      out.flush();
      System.out.println("Server: receives clientMessage->" + clientMessage);
    }
  } 

  /**
   * 基于BIO的Socket客户端线程
   *
   * @author shirdrn
   */
  static class SocketClient implements Runnable { 

    private String ipAddress;
    private int port;
    /** 待发送的请求数据 */
    private String data; 

    public SocketClient(String ipAddress, int port) {
      this.ipAddress = ipAddress;
      this.port = port;
    } 

    @Override
    public void run() {
      this.send();
    } 

    /**
     * 连接Socket服务端,并模拟发送请求数据
     */
    public void send() {
      Socket socket = null;
      OutputStream out = null;
      InputStream in = null;
      try {
        socket = new Socket(this.ipAddress, this.port); // 连接
        // 发送请求
        out = socket.getOutputStream();
        out.write(data.getBytes());
        out.flush();
        // 接收响应
        in = socket.getInputStream();
        int totalBytes = 0;
        int receiveBytes = 0;
        byte[] receiveBuffer = new byte[128];
        if((receiveBytes=in.read(receiveBuffer))!=-1) {
          totalBytes += receiveBytes;
        }
        String serverMessage = new String(receiveBuffer, 0, receiveBytes);
        System.out.println("Client: receives serverMessage->" + serverMessage);
      } catch (UnknownHostException e) {
        e.printStackTrace();
      } catch (IOException e) {
        e.printStackTrace();
      } catch (Exception e) {
        e.printStackTrace();
      } finally {
        try {
          // 发送请求并接收到响应,通信完成,关闭连接
          out.close();
          in.close();
          socket.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    } 

    public void setData(String data) {
      this.data = data;
    }
  } 

  public static void main(String[] args) throws Exception {
    SocketServer server = new SocketServer(1983);
    server.start(); 

    Thread.sleep(3000); 

    for(int i=0; i<threadCount; i++) {
      SocketClient client = new SocketClient("localhost", 1983);
      client.setData("I am the client " + (i+1) + ".");
      new Thread(client).start();
      Thread.sleep(0, 1);
    }
  }
}

经过测试,大约需要7110ms,大概接近7s,没有太大提高。

BIO通信改进

通过上面的测试我们可以发现,在Socket服务端对来自客户端的请求进行处理时,会发生阻塞,严重地影响了能够并发处理请求的效率。实际上,在Socket服务端接收来自客户端连接能力的范围内,可以将接收请求独立出来,从而在将处理请求独立粗话来,通过一个请求一个线程处理的方式来解决上述问题。这样,服务端是多处理线程对应客户端多请求,处理效率有一定程度的提高。

下面,通过单线程接收请求,然后委派线程池进行多线程并发处理请求:

/**
   * 基于BIO的Socket服务端进程
   *
   * @author shirdrn
   */
  static class SocketServer extends Thread { 

    /** 服务端口号 */
    private int port = 8888;
    /** 为客户端分配编号 */
    private static int sequence = 0;
    /** 处理客户端请求的线程池 */
    private ExecutorService pool; 

    public SocketServer(int port, int poolSize) {
      this.port = port;
      this.pool = Executors.newFixedThreadPool(poolSize);
    } 

    @Override
    public void run() {
      Socket socket = null;
      int counter = 0;
      try {
        ServerSocket serverSocket = new ServerSocket(this.port);
        boolean flag = false;
        Date start = null;
        while(true) {
          socket = serverSocket.accept(); // 监听
          // 有请求到来才开始计时
          if(!flag) {
            start = new Date();
            flag = true;
          }
          // 将客户端请求放入线程池处理
          pool.execute(new RequestHandler(socket));
          if(++counter==threadCount) {
            Date end = new Date();
            long last = end.getTime() - start.getTime();
            System.out.println(threadCount + " requests cost " + last + " ms.");
          }
        }
      } catch (IOException e) {
        e.printStackTrace();
      }
    } 

    /**
     * 客户端请求处理线程类
     *
     * @author shirdrn
     */
    class RequestHandler implements Runnable { 

      private Socket socket; 

      public RequestHandler(Socket socket) {
        this.socket = socket;
      } 

      @Override
      public void run() {
        try {
          InputStream in = socket.getInputStream(); // 流:客户端->服务端(读)
          OutputStream out = socket.getOutputStream(); // 流:服务端->客户端(写)
          int receiveBytes;
          byte[] receiveBuffer = new byte[128];
          String clientMessage = "";
          if((receiveBytes=in.read(receiveBuffer))!=-1) {
            clientMessage = new String(receiveBuffer, 0, receiveBytes);
            if(clientMessage.startsWith("I am the client")) {
              String serverResponseWords =
                "I am the server, and you are the " + (++sequence) + "th client.";
              out.write(serverResponseWords.getBytes());
            }
          }
          out.flush();
          System.out.println("Server: receives clientMessage->" + clientMessage);
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
  }

可见,这种改进方式增强服务端处理请求的并发度,但是每一个请求都要由一个线程去处理,大量请求造成服务端启动大量进程进行处理,也是比较占用服务端资源的。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Java中网络IO的实现方式(BIO、NIO、AIO)介绍

    在网络编程中,接触到最多的就是利用Socket进行网络通信开发.在Java中主要是以下三种实现方式BIO.NIO.AIO. 关于这三个概念的辨析以前一直都是好像懂,但是表达的不是很清楚,下面做个总结完全辨析清楚. 1. BIO方式 首先我用一个较为通俗的语言来说明: BIO 就是阻塞IO,每个TCP连接进来服务端都需要创建一个线程来建立连接并进行消息的处理.如果中间发生了阻塞(比如建立连接.读数据.写数据时发生阻碍),线程也会发生阻塞,并发情况下,N个连接需要N个线程来处理. 这种方式的缺点就是

  • Java中BIO、NIO、AIO的理解

    在高性能的IO体系设计中,有几个名词概念常常会使我们感到迷惑不解.具体如下: 1 什么是同步? 2 什么是异步? 3 什么是阻塞? 4 什么是非阻塞? 5 什么是同步阻塞? 6 什么是同步非阻塞? 7 什么是异步阻塞? 8 什么是异步非阻塞? 先来举个实例生活中的例子: 如果你想吃一份宫保鸡丁盖饭: 同步阻塞:你到饭馆点餐,然后在那等着,还要一边喊:好了没啊! 同步非阻塞:在饭馆点完餐,就去遛狗了.不过溜一会儿,就回饭馆喊一声:好了没啊! 异步阻塞:遛狗的时候,接到饭馆电话,说饭做好了,让您亲自

  • 基于BIO的Java Socket通信详解

    BIO,即阻塞IO,在基于Socket的消息通信过程中,Socket服务端向外部提供服务,而Socket客户端可以建立到Socket服务端的连接,进而发送请求数据,然后等待Socket服务端处理,并返回处理结果(响应). 基于BIO的通信,Socket服务端会发生阻塞,即在监听过程中每次accept到一个客户端的Socket连接,就要处理这个请求,而此时其他连接过来的客户端只能阻塞等待.可见,这种模式下Socket服务端的处理能力是非常有限的,客户端也只能等待,直到服务端空闲时进行请求的处理.

  • Android Socket通信详解

    一.Socket通信简介  Android与服务器的通信方式主要有两种,一是Http通信,一是Socket通信.两者的最大差异在于,http连接使用的是"请求-响应方式",即在请求时建立连接通道,当客户端向服务器发送请求后,服务器端才能向客户端返回数据.而Socket通信则是在双方建立起连接后就可以直接进行数据的传输,在连接时可实现信息的主动推送,而不需要每次由客户端想服务器发送请求. 那么,什么是socket?Socket又称套接字,在程序内部提供了与外界通信的端口,即端口通信.通过

  • Java Socket编程详解及示例代码

    Socket,又称为套接字,Socket是计算机网络通信的基本的技术之一.如今大多数基于网络的软件,如浏览器,即时通讯工具甚至是P2P下载都是基于Socket实现的.本文会介绍一下基于TCP/IP的Socket编程,并且如何写一个客户端/服务器程序. 餐前甜点 Unix的输入输出(IO)系统遵循Open-Read-Write-Close这样的操作范本.当一个用户进程进行IO操作之前,它需要调用Open来指定并获取待操作文件或设备读取或写入的权限.一旦IO操作对象被打开,那么这个用户进程可以对这个

  • Java线程通信详解

    线程通信用来保证线程协调运行,一般在做线程同步的时候才需要考虑线程通信的问题. 1.传统的线程通信 通常利用Objeclt类提供的三个方法: wait() 导致当前线程等待,并释放该同步监视器的锁定,直到其它线程调用该同步监视器的notify()或者notifyAll()方法唤醒线程. notify(),唤醒在此同步监视器上等待的线程,如果有多个会任意选择一个唤醒 notifyAll() 唤醒在此同步监视器上等待的所有线程,这些线程通过调度竞争资源后,某个线程获取此同步监视器的锁,然后得以运行.

  • 详解Java Socket通信封装MIna框架

    核心类 IoService :Mina中将服务端和客户端都看成是服务,这里提供统一接口IoService,这个接口的作用就是用来处理套接字机制.也正是IoService来监听消息返回消息这些步骤,可以说IoService就是我们Mina中核心 IoProcessor:这个接口在另一个线程上,负责检查是否有数据在通道上读写,也就是说它也拥有自己的Selector,这是与我们使用JAVA NIO 编码时的一个不同之处,通常在JAVA NIO 编码中,我们都是使用一个Selector,也就是不区分Io

  • JAVA实现基于皮尔逊相关系数的相似度详解

    最近在看<集体智慧编程>,相比其他机器学习的书籍,这本书有许多案例,更贴近实际,而且也很适合我们这种准备学习machinelearning的小白. 这本书我觉得不足之处在于,里面没有对算法的公式作讲解,而是直接用代码去实现,所以给想具体了解该算法带来了不便,所以想写几篇文章来做具体的说明.以下是第一篇,对皮尔逊相关系数作讲解,并采用了自己比较熟悉的java语言做实现. 皮尔逊数学公式如下,来自维基百科. 其中,E是数学期望,cov表示协方差,\sigma_X和\sigma_Y是标准差. 化简后

  • Java基于TCP协议的Socket通信

    目录 简介 TCP简介 JAVA Socket简介 SocketImpl介绍 TCP 编程 构造ServerSocket 1.1 绑定端口 1.2 设定客户连接请求队列的长度 1.3 设定绑定的IP 地址 1.4 默认构造方法的作用 多线程示例 简介 TCP简介 TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的.可靠的.基于字节流的传输层通信协议,由IETF的RFC 793定义.在简化的计算机网络OSI模型中,它完成第四层传输层所指定的功能,用户

  • JAVA基于Slack实现异常日志报警详解

    目录 一.功能介绍 二.Slack介绍 三.前期准备 slack配置 pom.xml 四.具体实现 1.实现Slack发送消息 SlackUtil 给Slack发消息工具类 向 webhook发起请求通过Urlencode SlackUtil测试 2.重写打印日志类 常见异常打日志处理 重写封装打印日志的方法 测试日志类 五.优化扩展想法 其他代码 一.功能介绍 在我们日常开发中,如果系统在线上环境上,发生异常,开发人员不能及时知晓来修复,可能会造成重大的损失,因此后端服务中加入异常报警的功能是

  • Java 多线程实例详解(三)

    本文主要接着前面多线程的两篇文章总结Java多线程中的线程安全问题. 一.一个典型的Java线程安全例子 public class ThreadTest { public static void main(String[] args) { Account account = new Account("123456", 1000); DrawMoneyRunnable drawMoneyRunnable = new DrawMoneyRunnable(account, 700); Thr

  • 基于IntBuffer类的基本用法(详解)

    废话不多说,直接上代码 package com.ietree.basicskill.socket.basic.nio; import java.nio.IntBuffer; /** * Created by Administrator on 2017/5/25. */ public class BufferTest { public static void main(String[] args) { // 1 基本操作 /*//创建指定长度的缓冲区 IntBuffer buf = IntBuff

随机推荐