Java SE 高级 - Socket网络编程

第一章:
网络编程:就是在一定的协议下,实现两台计算机之间的通信的程序。
** 一、软件结构**
C/S 结构 :全称为Client/Sever 结构 ,是指客户端和服务器结构。常见的软件有QQ、迅雷等软件。
弊端:客户端是需要下载使用,不利于推广。

B/S结构 : 全称为Browser/Sever 结构 是指浏览器和服务器结构 常见的浏览器有谷歌和火狐。
优势:无需更新客户端浏览器,也可以直接看到新版本

网络通信协议:
网络通信协议:通信协议是对计算机必须遵守的规则,只有遵守这些规则,计算机之间才能进行通信。协议中对数据的传输格式、传输速率、传输步骤等做了统一的规定,通信双方必须同时遵守,最终完成数据交换。
TCP/IP协议 :传输控制协议/因特网互联协议(Transmission Control Protocol/Internet Protocol ),是Internet最基本、最广泛的协议。它定义了计算机如何连入因特网,以及数据如何在他们之间传输的标准。它的内部包含一系列的用于处理数据通信的协议,并采用了4层的分层模型,每一层都呼叫它的下一层所提供的协议来完成自己的需求。

协议分类:
** 应用层:**
HTTP: 超文本传输协议,网页传输
FTP:文件传输
SMTP:基本的邮件传输

** 传输层:**
TCP:传输控制协议(Transmission Control Protocol)面向连接的安全的数据传输协议
UDP:用户数据报协议(User Datagram Protocol)面向无连接的不安全的数据传输协议。
数据链路层:硬件层面上

TCP和UDP的区别:
1.TCP:传输控制协议(Transmission Control Protocol) 面向连接的安全数据传输协议
UDP:用户数据报协议(User Datagram Protocol )面向无连接的不安全的数据传输协议。
2.TCP协议可以保证传输数据的安全,传输速度相对较低
UDP协议它是不可靠协议,因为无连接,所以传输速度快,但是容易丢失数据。
3.TCP使用场景:文件上传、下载、消息传递等。
UDP使用场景:语音聊天、视频聊天等。
UDP协议的特点:
面向无连接的协议
发送端只管发送,不确认对方是否能收到。
基于数据包进行数据传输。
发送数据的大小限制64K以内。
因为面向无连接,速度快,但是不可靠
UDP协议的使用场景:
即时通讯
在线视频
网络语音通话
UDP协议相关的两个类
DatagramPacket 数据包对象
作用:用来封装要发送或要接收的数据,比如:集装箱
DatagramSocket 发送/接收对象
作用:用来发送或接收数据包,比如:码头
UDPClient 和 UDPSever 之间通信的原理图:

网络编程三要素:
1.协议:计算机网络必须遵守的规则
2.IP地址:指互联网协议地址(Internet Protocol Adress),俗称IP。IP地址用来给一个网络中的计算机设备做唯一的编号。假如我们把“个人电脑”比作“一台电话”的话,那么"IP地址"就相当于“电话号码”。
IPv4:是一个32位的二进制数,通常被分为4个字节,表示成a.b.c.d 的形式,例如192.168.65.100 。其中a、b、c、d都是0~255之间的十进制整数,那么最多可以表示42亿个。
网络的分类有多种形式,从网络的地理覆盖范围可以将网络分为三类,即局域网、城域网、广域网。
局域网: 局域网(LAN)是指在某一区域内由多台计算机相互连接形成的计算机网络,其覆盖范围为几百米到几千米之间。局域网常被用于连接公司办公室或工厂中的个人计算机,以便共享资源(例如打印机资源的共享)和交换信息。 一般多用于公司内部使用。
城域网: 城域网(MAN),是一种大型的局域网,采用和局域网类似的技术。城域网覆盖面积比局域网略广,可以达到几十千米,其传输速率也高于局域网。 一般多用于城市之间使用。
广域网(公网): 广域网(WAN)也叫远程网,是一种地理范围巨大的网络,它将分布在不同地区的局域网或计算机系统互连起来,达到资源共享的目的。通常广域网的覆盖范围可达到几万千米,一般由通信公司建立和维护。例如,国家之间建立的网络都是广域网。 一般可以在全球任何地方访问。
IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。有资料显示,全球IPv4地址在2011年2月分配完毕。
为了扩大地址空间,拟通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,表示成ABCD:EF01:2345:6789:ABCD:EF01:2345:6789,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。
常用命令
查看本机IP地址,在控制台输入:
ipconfig

检查网络是否连通,在控制台输入:
ping 空格 IP地址
ping 220.181.57.216 //IP
ping www.baidu.com //域名

特殊的IP地址
本机IP地址:127.0.0.1、localhost 。不受环境的影响,任何时候都存在这两个IP,可以直接找本机。
3.端口号
网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这些进程呢?
如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的进程(应用程序)了。
端口号:用两个字节表示的整数,它的取值范围是065535。其中,01023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。
利用协议+IP地址+端口号 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。

InetAdress 类 : 一个该类的对象就代表一个IP地址对象。

常用方法:
//获得本地主机IP地址对象
static InetAdress getLocalHost()

//根据IP地址字符串或主机名获得对应的IP地址对象
static InetAdress getByName(String host)

//获得主机名
String getHostName()

//获得IP地址字符串
String getHostAdress()

TCP通信程序:
TCP协议是面向连接的通信协议,即在传输数据前先在客户端和服务端建立逻辑连接,然后再传输数据。他提供了两台计算机之间可靠无差错的数据传输。
TCP通信过程如下图所示:

TCP==>Transfer control protocol ==>传输控制协议
TCP协议的特点
面向连接的协议
只能由客户端主动发送数据给服务器端,服务器端接收到数据之后,可以给客户端响应数据。
通过三次握手建立连接,连接成功形成数据传输通道。
通过四次握手断开连接
基于IO流进行数据传输
传输数据没有大小限制
因为面向连接的协议,速度慢,但是是可靠协议。
TCP协议的使用场景
文件的上传和下载
邮件发送和接收
远程登录
**TCP协议相关的类 **
Socket
一个该类的对象就代表一个客户端程序
ServerSocket
一个该类的对象就代表一个服务端程序。
Socket 类构造方法
Socket(String host,int port)
根据ip地址字符串和端口号创建客户端socket对象
PS:只要执行该方法,就会立即连接指定的服务器程序,如果连接不成功,则会抛出异常。
Socket类常用方法
OutputStream getOutputStream(); 获得字节输出流对象
InputStream getInputStream(); 获得字节输入流对象

TCP通信案例:
客户端:
class Socket implments Closeable Socket资源
客户端步骤:
创建Socket套接字
通过Socket套接字获得输出字节流getOutputStream(),来进行写出数据。
通过流对象,写出数据。
服务端:
class ServerSocket implments Closeable ServerSocket 资源
服务端步骤:
创建服务端的套接字ServerSocket,注册端口号
等待接入客户端 accept()
通过Socket套接字,获得输入字节流getInputStream(),来进行读取信息
通过流对象,读入消息。

需求1:服务端接收一条消息,客户端发送一条消息
/**

  • 客户端
    **/
    public class TCPClient {
    public static void main(String[] args) {
    System.out.println("---------客户端------------");
    try (
    //1.创建Socket套接字
    Socket socket = new Socket("127.0.0.1",7788);
    //2.通过Socket套接字,获得输出字节流,来进行写出消息
    OutputStream outputStream = socket.getOutputStream();
    //创建高效流
    PrintWriter pw = new PrintWriter(outputStream);
    ){
    //客户端发送一条消息
    pw.println("早晨好,吃饭了么?");
    pw.flush();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }

/**

  • 服务端
    **/
    public class TCPServer {

    public static void main(String[] args) {
    System.out.println("---------服务端------------");

      try (
              //1.创建服务端的套接字,注册端口号
              ServerSocket ss = new ServerSocket(7788);
              //2.等待接收客户端的接入
              Socket socket = ss.accept();
              //3.通过Socket套接字,获得输入字节流,来进行读取消息
              InputStream inputStream = socket.getInputStream();
              //创建了一个转换流
              InputStreamReader reader = new InputStreamReader(inputStream);
              //创建高效流
              BufferedReader br = new BufferedReader(reader);
      ) {
          System.out.println("客户端成功接入");
          //服务端读取消息
          String s = br.readLine();
          //socket.getRemoteSocketAddress() 获得当前接入的客户端的IP地址对象
          System.out.println(socket.getRemoteSocketAddress() + "说:" + s);
      } catch (IOException e) {
          e.printStackTrace();
      }
    

    }

}

需求2:服务端接收多条消息,客户端发送多条消息
/**

  • 客户端
    */
    public class TCPClient {
    public static void main(String[] args) {
    System.out.println("---------客户端------------");
    try (
    //1.创建Socket套接字
    Socket socket = new Socket("127.0.0.1",7788);
    //2.通过Socket套接字,获得输出字节流,来进行写出消息
    OutputStream outputStream = socket.getOutputStream();
    //创建高效流
    PrintWriter pw = new PrintWriter(outputStream);
    Scanner sc = new Scanner(System.in);
    ){
    //客户端发送多条消息
    System.out.print("请说:");
    String s = sc.nextLine();
    while (s!=null){
    pw.println(s);
    pw.flush();

              if(s.equals("bye")){
                  break;
              }
              System.out.print("请说:");
              s = sc.nextLine();
          }
      } catch (IOException e) {
          e.printStackTrace();
      }
    

    }
    }

/**

  • 服务端
    */
    public class TCPServer {
    public static void main(String[] args) {
    System.out.println("---------服务端------------");
    try (
    //1.创建服务端的套接字,注册端口号
    ServerSocket ss = new ServerSocket(7788);
    //2.等待接收客户端的接入
    Socket socket = ss.accept();
    //3.通过Socket套接字,获得输入字节流,来进行读取消息
    InputStream inputStream = socket.getInputStream();
    //创建了一个转换流
    InputStreamReader reader = new InputStreamReader(inputStream);
    //创建高效流
    BufferedReader br = new BufferedReader(reader);
    ){
    System.out.println("客户端成功接入");
    //服务端读取多条消息
    String s;
    while((s = br.readLine())!=null){
    //socket.getRemoteSocketAddress() 获得当前接入的客户端的IP地址对象
    if(s.equals("bye")){
    System.out.println(socket.getRemoteSocketAddress()+"下线了!");
    }else{
    System.out.println(socket.getRemoteSocketAddress()+"说:"+s);
    }
    }
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }

需求3:服务端可以接收多条消息,也可以发送消息;客户端可以发送多条消息,也可以接收消息 (一对一,阻塞的现象)
/**

  • 客户端
    */
    public class TCPClient {
    public static void main(String[] args) {
    System.out.println("----------客户端----------");
    try (
    //创建Socket套接字,通过getOutPutStream 写出数据
    Socket socket = new Socket(InetAddress.getLocalHost(),7788);
    PrintWriter pw = new PrintWriter(socket.getOutputStream());
    //通过getInputStream 写入数据
    BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    Scanner sc = new Scanner(System.in);
    ){
    //先写信息,再读取信息
    while (true){
    //写出信息
    String str = sc.nextLine();
    pw.println(str);
    pw.flush();
    if (str.equals("exit")){
    break;
    }
    //读入信息
    String s = br.readLine();
    if (s.equals("exit")){
    System.out.println(socket.getRemoteSocketAddress()+"下线了!");
    }else{
    System.out.println(socket.getRemoteSocketAddress()+"说:"+s);
    }
    }
    } catch (UnknownHostException e) {
    e.printStackTrace();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }

/**

  • 服务端
    */
    public class TCPServer {
    public static void main(String[] args) {
    System.out.println("---------服务端----------");
    try (
    //创建ServerSocket 注册端口号
    ServerSocket ss = new ServerSocket(7788);
    //等待接入客户端
    Socket socket = ss.accept();
    BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    //获取输出字节流,写出信息
    PrintWriter pw = new PrintWriter(socket.getOutputStream());
    Scanner sc = new Scanner(System.in)
    ){
    System.out.println("---------客户端已接入-----------");
    //先读再写
    while (true){
    //先发送信息
    String s = br.readLine();
    if (s.equals("exit")){
    System.out.println(socket.getRemoteSocketAddress()+"下线了!");
    }else{
    System.out.println(socket.getRemoteSocketAddress()+"说:"+s);
    }
    //再读取信息
    String str = sc.nextLine();
    pw.println(str);
    pw.flush();
    if (str.equals("exit")){
    break;
    }
    }
    } catch (IOException e) {
    e.printStackTrace();
    }

    }
    }

需求4:服务端可以接收多条消息,也可以发送消息;客户端可以发送多条消息,也可以接收消息 (一对一,非阻塞的现象)
/**

  • 读线程
    */
    public class ReadThread extends Thread {
    private Socket socket;

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

    @Override
    public void run() {
    try (
    BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    ){
    String line;
    while((line = br.readLine())!=null){
    if(line.equals("exit")){
    System.out.println(socket.getRemoteSocketAddress()+"已下线");
    }else{
    System.out.println(socket.getRemoteSocketAddress()+"说:"+line);
    }
    }
    } catch (IOException e) {
    System.out.println("我下线了~~~");
    }
    }
    }

/**

  • 写线程
    */
    public class WriteThread extends Thread {
    private Socket socket;

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

    @Override
    public void run() {
    try(
    PrintWriter pw = new PrintWriter(socket.getOutputStream());
    Scanner sc = new Scanner(System.in);
    ){
    while (true){
    String s = sc.nextLine();
    pw.println(s);
    pw.flush();

             if(s.equals("exit")){
                 break;
             }
         }
     } catch (IOException e) {
         e.printStackTrace();
     }
    

    }
    }

/**

  • 客户端
    */
    public class TCPClient {
    public static void main(String[] args) {
    System.out.println("---------客户端------------");
    try{
    //1.创建Socket套接字 "127.0.0.1"服务端所在的IP 7788服务端注册的端口号
    Socket socket = new Socket("127.0.0.1",7788);
    //2.开启读、写线程
    new ReadThread(socket).start();
    new WriteThread(socket).start();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }

/**

  • 服务端
    **/
    public class TCPServer {
    public static void main(String[] args) {
    System.out.println("---------服务端------------");
    try {
    //1.创建服务端的套接字,注册端口号
    ServerSocket ss = new ServerSocket(7788);
    //2.等待接收客户端的接入
    Socket socket = ss.accept();
    System.out.println("客户端成功接入");
    //3.开启读、写线程
    new ReadThread(socket).start();
    new WriteThread(socket).start();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }

需求5:当服务器只是读取信息,客户端写出信息,此时一个服务器可以接收多个客户端
/**

  • 客户端
    */
    public class TCPClient {
    public static void main(String[] args) {
    System.out.println("----------客户端----------");
    try (
    Socket socket = new Socket(InetAddress.getLocalHost(), 7788);
    PrintWriter pw = new PrintWriter(socket.getOutputStream());
    Scanner sc = new Scanner(System.in);
    ) {
    while (true) {
    String str = sc.nextLine();
    if (str.equals("exit")) {
    break;
    }
    pw.println(str);
    pw.flush();
    }
    } catch (UnknownHostException e) {
    e.printStackTrace();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }

/**

  • 服务端
    */
    public class TCPServer {
    public static void main(String[] args) {
    System.out.println("----------服务端----------");
    try {

          ServerSocket ss = new ServerSocket(7788);
          while (true) {
              Socket socket = ss.accept();
              System.out.println(socket.getRemoteSocketAddress() + "已上线!");
              new Thread(new ReadThread(socket)).start();
          }
      } catch (IOException e) {
          e.printStackTrace();
      }
    

    }
    }

/**

  • 读线程
    */
    public class ReadThread implements Runnable {
    private Socket socket;
    public ReadThread(Socket socket){
    this.socket = socket;
    }
    @Override
    public void run() {
    try (
    BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    ){
    String line;
    while ((line=br.readLine())!=null){
    if (line.equals("exit")){
    System.out.println(socket.getRemoteSocketAddress()+"已下线!");
    }else {
    System.out.println(socket.getRemoteSocketAddress()+"说:"+line);
    }
    }
    } catch (IOException e) {
    System.out.println(socket.getRemoteSocketAddress()+"拜拜嘞~~~");
    }
    }
    }

需求6:当服务器只是读取信息,客户端写出信息,此时一个服务器可以接收多个客户端(采用的策略:服务端为每个接入成功的客户端,让线程池中的一条线程去进行维护之间的管道通信)

需求7:客户端写出信息,此时服务器转发信息给其它客户端,其它客户端读取消息

文件上传案例:
文件上传分析图解:

【客户端】:输入流,从硬盘读取文件数据到程序中。
【客户端】:输出流,写文件数据到服务端
【服务端】:输入流,读取文件数据到服务端程序
【服务端】:输出流,写出文件数据到服务器硬盘中
【服务端】:获取输出流,回写数据。
【客户端】:获取输入流,解析回写数据。

案例实现:
服务端实现:
public class UploadServer {
public static void main(String[] args) throws IOException {
//创建线程池
ThreadPool pool = new ThreadPool(5,50);

    //获得服务套接字,注册端口号
    ServerSocket ss = new ServerSocket(8888);
    System.out.println("-----服务器启动-----");
    //接收客户端
    while (true){
        Socket socket = ss.accept();
        System.out.println("已经成功接入客户端");
        pool.execute(new MyRun(socket));
    }
}

}
class MyRun implements Runnable{
private Socket socket;
public MyRun(Socket socket){
this.socket = socket;
}

@Override
public void run() {
    try {
        //3.先将图片信息通过通信管道Socket,从客户端读到服务端
        BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());

        //4.将图片写出至服务器所在的本地磁盘上
        BufferedOutputStream bos = new BufferedOutputStream(
                new FileOutputStream(new File("E:\\abc", UUID.randomUUID().toString()+".jpg")));
        //边读边写:(复制)
        byte[] bytes = new byte[1024];
        int i=0;
        while((i=bis.read(bytes))!=-1){
            bos.write(bytes,0,i);
        }
        bos.flush();
        bos.close();
        //5.给客户端写回一个上传完成的信息
        PrintWriter pw = new PrintWriter(socket.getOutputStream());
        pw.println("上传完成~~~");
        pw.flush();
    } catch (IOException e) {
        System.out.println("拜拜嘞");
    }
}

}

客户端实现:
public class UploadClient {
public static void main(String[] args) throws IOException {
System.out.println("----------客户端-----------");

    //1.将图片信息读取到内存中
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\aaa\\0.jpg"));

    //2.通过通信管道Socket,写出至服务器端
    //创建Socket套接字,创建客户端
    Socket socket = new Socket("127.0.0.1",8888);

    //通过socket的对象,创建将图片信息写出至服务端的流对象
    BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());

    //边读边写:(复制)
    byte[] bytes = new byte[1024];
    int i=0;
    while((i=bis.read(bytes))!=-1){
        bos.write(bytes,0,i);
    }
    bos.flush();
    bis.close();
    //发送一个信号 - 告知服务器端已写完
    socket.shutdownOutput();
    System.out.println("客户端上传图片....");

    //6.读取图片上传成功的消息
    BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    String s = br.readLine();
    System.out.println(s);
}

}

自定义一个线程池:
public class ThreadPool {

//线程池对象
private ExecutorService pool;

/**
 * 创建自定义的线程池的构造方法
 * @param maxPoolSize  线程池中允许的最大线程数量
 * @param queueSize    阻塞任务队列中,允许等待的任务数量
 */
public ThreadPool(int maxPoolSize, int queueSize){
    pool = new ThreadPoolExecutor(
            3,  //线程池中核心的线程数
            maxPoolSize,    //线程池中允许的最大线程数量
            60L, //线程池中超过corePoolSize大小后,多余的空闲线程等待新任务的最长时间
            TimeUnit.SECONDS,  //keepAliveTime参数的时间单位
            new ArrayBlockingQueue<Runnable>(queueSize),  //阻塞任务队列对象
            new ThreadPoolExecutor.AbortPolicy()  //拒绝策略
    );
}

//执行任务的方法
public void execute(Runnable task){
    pool.execute(task);
}

}

模拟B/S服务器:

模拟网站服务器,使用浏览器访问自己编写的服务端程序,查看网页效果。
案例分析:
1.准备页面数据,web文件夹。
2.我们模拟服务器端,ServerSocket类监听端口,使用浏览器访问,查看网页效果。

案例实现:
public class BSServer {
public static void main(String[] args) {
try {
//创建服务套接字,注册端口号,接收客户端
ServerSocket ss = new ServerSocket(7788);
System.out.println("服务器已开启");
ThreadPool pool = new ThreadPool(5,50);
while (true){
//已经接入客户端
Socket socket = ss.accept();
System.out.println("已经接入客户端");

            //从线程池中拿取线程执行任务
            pool.execute(()->{
                //执行的任务,响应一个页面给客户
                try {
                    //创建输出的IO流
                    PrintStream ps = new PrintStream(socket.getOutputStream());
                    //写回遵循的协议 HTTP  网页传输协议
                    ps.println("HTTP/1.1 200 OK");
                    //写回响应文本的内容text/html,指定编码集中文友好UTF-8
                    ps.println("Content-Type:text/html;charset=utf-8");
                    //写回响应的空行
                    ps.println();
                    //写回响应正文
                    ps.println("<h1 style='color:red'>写成功啦!!!</h1>");
                    ps.println("<ul><li>111</li><li>222</li><li>333</li></ul>");
                    ps.flush();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        }

    } catch (IOException e) {
        e.printStackTrace();
    }
}

}

NIO
了解关键词:
同步与异步(synchronous/asynchronous):同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步;而异步则相反,其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系。
阻塞与非阻塞:在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如ServerSocket 新连接建立完毕,或者数据读取、写入操作完成;而非阻塞则是不管IO操作是否结束,直接返回,相应操作在后台继续处理。
1.I/O模型
概念:I/O模型简单的理解,就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。

Java支持三种网络编程模型I/O模式
1)、Java BIO:同步并阻塞(传统阻塞型),服务器实现模式作为一个连接一个线程,即客户端有连接请求时,服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销
2)、Java NIO :同步非阻塞,服务器实现模式作为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器(Selector选择器)上,多路复用器轮询到连接有I/O请求就进行处理。
3)Java AIO(NIO.2):异常非阻塞,AIO引入异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,它的特点是由操作系统完成后才通知服务端气动线程去处理,一般适用于连接数较多且连接时间较长的应用。JDK1.7才出现,尚未广泛应用。

适用场景分析
1)、BIO 方式适用于连接数目比较小且固定的架构,BIO这种当时对服务器资源要求比较高,并发句限于应用中,JDK1.4以前的唯一选择,虽然BIO方式不适合处理过多的高并发,但程序简单易理解。

2)、NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器、弹幕系统、服务器系统、服务器间通讯等。编程比较复杂,JDK1.4开始支持。

3)、AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS(操作系统)参与并发操作。编程比较复杂,JDK1.7开始支持。

Java BIO概述
Java BIO 基本介绍
1)、Java BIO 就是传统的java io编程,其相关的类和接口在java.io包下。
2)、BIO(Blocking I/O:同步阻塞,服务器实现模式作为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善,这个线程池机制主要是实现多客户端连接服务器,但并不能减少线程的个数,即时使用线程池,仍然会存在大量任务阻塞在队列的情况。
3)、BIO方式适用于连接数目比较小且固定的架构,BIO这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,虽然BIO方式不适合处理过多的高并发,但程序简单易理解。

Java BIO 编程流程
1)、服务器启动ServerSocket
2)、客户端启动Socket对服务器进行通信,默认情况下服务器端需要对每个客户端建立一个线程与之通信
3)、客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待,或者拒绝。
4)、若服务器有响应,客户端线程会等待请求结束后,再继续执行。

Java BIO 应用实例
需求:
1)、使用BIO模型编写一个服务器端,监听6666端口,当有客户端连接时,就启动一个线程与之通信。
2)、要求使用线程池机制改善,可以连接多个客户端。
3)、服务器可以接受客户端发送数据(也可以使用telnet方式,则无需再编写客户端)

1.什么是Telnet?
  对于Telnet的认识,不同的人持有不同的观点,可以把Telnet当成一种通信协议,但是对于入侵者而言,Telnet只是一种远程登录的工具。一旦入侵者与远程主机建立了Telnet连接,入侵者便可以使用目标主机上的软、硬件资源,而入侵者的本地机只相当于一个只有键盘和显示器的终端而已。
Telnet协议是TCP/IP协议家族中的一员,是Internet远程登陆服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的能力。在终端使用者的电脑上使用telnet程序,用它连接到服务器。终端使用者可以在telnet程序中输入命令,这些命令会在服务器上运行,就像直接在服务器的控制台上输入一样。可以在本地就能控制服务器。要开始一个telnet会话,必须输入用户名和密码来登录服务器。Telnet是常用的远程控制Web服务器的方法。

2.怎么执行telnet 命令?
  1、点击开始 → 运行 → 输入CMD,回车。
  2、在出来的DOS界面里,输入telnet测试端口命令: telnet IP 端口 或者 telnet 域名 端口,回车。
  如果端口关闭或者无法连接,则显示不能打开到主机的链接,链接失败;端口打开的情况下,链接成功,则进入telnet页面(全黑的),证明端口可用。

3.Telnet 客户端命常用命令?
  open : 使用 openhostname 可以建立到主机的 Telnet 连接。

  close : 使用命令 close 命令可以关闭现有的 Telnet 连接。

  display : 使用 display 命令可以查看 Telnet 客户端的当前设置。

  send : 使用 send 命令可以向 Telnet 服务器发送命令。

windows系统修改cmd窗口utf-8编码格式 直接输入“chcp 65001”,回车键(Enter键)执行,这时候该窗口编码已经是UTF-8编码了。

Java BIO 问题分析:
每个请求(客户端)都需要创建独立的线程,与对应的客户端进行数据Read,业务处理,数据Write。
当并发数较大时,需要创建大量线程来处理连接,系统资源占用比较大
连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在Read操作上,造成线程资源浪费。

Java NIO概述
Java NIO 基本介绍
Java NIO 全称java non-blocking IO,是指JDK提供的新API。从JDK1.4开始,Java提供了一系列改进的输入/输出的新特性,被统称为NIO(New IO),是同步非阻塞的。
NIO 相关的类都被放在java.nio 包及子包下,并且对原java.io 包中的很多类进行改写。
NIO 有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)
NIO 是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩网络。
Java NIO 的非阻塞模式,使一个线程从某个通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不想之前的阻塞IO那样,非得分配10000个。
HTTP2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1 大了好几个数量级。

NIO和BIO的比较
BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O的效率比流I/O高很多
BIO是阻塞的,NIO是非阻塞的
BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写到通道中。Selector(选择器)用于监听多个通道事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。

NIO 三大核心原理示意图
一张图描述NIO的Selector、Channel 和Buffer 的关系

Selector 、Channel 和Buffer的关系图(简单版)
关系图的说明:
每个channel都会对应一个Buffer
Selector对应一个线程,一个线程对应多个channel(连接)
该图反应了有三个channel注册到g该Selector。
程序切换到哪个channel是有事件决定的,Event就是一个重要概念
Selector会根据不同的事件,在各个通道上切换
Buffer就是一个内存块,底层是有一个数组
数据的读取写入是通过Buffer,这个和BIO,BIO中要么是输入流,或者是输出流,不能双向,但是NIO的Buffer是可以读也可以写,需要flip方法切换
channel 是双向的,可以返回底层操作系统的情况,比如Linux,底层的操作系统通道就是双向的
Buffer缓冲区
缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer。

Buffer 类及其子类
在NIO中,Buffer是一个顶层父类,它是一个抽象类
常用Buffer子类:

ByteBuffer 存储字节数据到缓冲区
ShortBuffer 存储字符串数据到缓冲区
CharBuffer 存储字符数据到缓冲区
IntBuffer 存储整数数据到缓冲区
LongBuffer 存储长整型数据到缓冲区
DoubleBuffer 存储小数到缓冲区
FloatBuffer 存储小数到缓冲区

2.Buffer类定义了所有的缓冲区都具有的四个属性来提供关于其所含的数据元素的信息
// Invariants: mark <= position <= limit <= capacity
private int mark = -1; //标记
private int position = 0; //当前操作的下标
private int limit; // 操作下标的上限(取不到)
private int capacity; //容量

capacity :容量,即可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变
limit:表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的
position:位置,下一个要被读或写的元素的索引,每次读写缓冲区时都会改变值,为下次读写准备
mark: 标记

Buffer类相关方法一览
public abstract class Buffer {
//JDK1.4时,引入的api
public final int capacity( )//返回此缓冲区的容量
public final int position( )//返回此缓冲区的位置
public final Buffer position (int newPositio)//设置此缓冲区的位置
public final int limit( )//返回此缓冲区的限制
public final Buffer limit (int newLimit)//设置此缓冲区的限制
public final Buffer mark( )//在此缓冲区的位置设置标记
public final Buffer reset( )//将此缓冲区的位置重置为以前标记的位置
public final Buffer clear( )//清除此缓冲区, 即将各个标记恢复到初始状态,但是数据并没有真正擦除, 后面操作会覆盖
public final Buffer flip( )//反转此缓冲区
public final Buffer rewind( )//重绕此缓冲区
public final int remaining( )//返回当前位置与限制之间的元素数
public final boolean hasRemaining( )//告知在当前位置和限制之间是否有元素
public abstract boolean isReadOnly( );//告知此缓冲区是否为只读缓冲区

//JDK1.6时引入的api
public abstract boolean hasArray();//告知此缓冲区是否具有可访问的底层实现数组
public abstract Object array();//返回此缓冲区的底层实现数组
public abstract int arrayOffset();//返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量
public abstract boolean isDirect();//告知此缓冲区是否为直接缓冲区

}

ByteBuffer
对于Java中的基本数据类型(boolean除外),都有一个Buffer类型与之对应,最常用的自然是ByteBuffer类(二进制数据)
该类的主要方法:
public abstract class ByteBuffer {
//缓冲区创建相关api
public static ByteBuffer allocateDirect(int capacity)//创建直接缓冲区
public static ByteBuffer allocate(int capacity)//设置缓冲区的初始容量
public static ByteBuffer wrap(byte[] array)//把一个数组放到缓冲区中使用
//构造初始化位置offset和上界length的缓冲区
public static ByteBuffer wrap(byte[] array,int offset, int length)
//缓存区存取相关API
public abstract byte get( );//从当前位置position上get,get之后,position会自动+1
public abstract byte get (int index);//从绝对位置get
public abstract ByteBuffer put (byte b);//从当前位置上添加,put之后,position会自动+1
public abstract ByteBuffer put (int index, byte b);//从绝对位置上put
}

Channel通道

基本介绍
NIO的通道类似于流,但有些区别如下:
通道可以同时进行读写,而流只能读或者只能写
通道可以实现异步读写数据
通道可以从缓冲读取数据,也可以写数据到缓冲:

2.BIO 中的Stream是单向的,例如FileInputStream 对象只能进行读取数据的操作,而NIO中的通道(Channel)是双向的,可以读操作,也可以写操作。
3.Channel在NIO中是一个接口public interface Channel extends Closeable{}
4.常用的Channel类有:FileChannel、DatagramChannel、ServerSocketChannel和SocketChannel。【ServerSocketChannel类似ServerSocket,SocketChannel类似Socket】
5.FileChannel 用于文件的数据读写,DatagramChannel 用于UDP的数据读写,ServerSocketChannel 和 SocketChannel用于TCP的数据读写。

FileChannel 类
FileChannel 主要用来对本地的文件进行IO操作,常见的方法有:
public int read(ByteBuffer dst) ,从通道读取数据并放到缓冲区中

public int write(ByteBuffer src) ,把缓冲区的数据写到通道中

public long transferFrom(ReadableByteChannel src, long position, long count),从目标通道中复制数据到当前通道

public long transferTo(long position, long count, WritableByteChannel target),把数据从当前通道复制给目标通道

关于Buffer和Channel的注意事项和细节
ByteBuffer支持类型化的put和get,put放入的是什么数据类型,get就应该使用相应的数据类型来取出,否则可能有bufferUnderflowException 异常

2.可以将一个普通Buffer转成只读Buffer
import java.nio.ByteBuffer;

/**

  • 只读buffer,顾名思义只能用来读,很有用的,让别人不能修改,只能读

  • 只读buffer底层是:HeapByteBufferR,而普通buffer底层是:HeapByteBuffer

  • 如果向里面put数据会抛出异常,看源码会发现,他的put方法就是直接抛异常。

  • 可以debug 一下
    */
    public class ReadOnlyBuffer {
    public static void main(String[] args) {

     ByteBuffer byteBuffer = ByteBuffer.allocate(10);
    
     for (int i=0;i<10;i++){
         byteBuffer.put((byte) i);
     }
    
     byteBuffer.flip();
    
     ByteBuffer onlyReadBuffer=byteBuffer.asReadOnlyBuffer();
     System.out.println(onlyReadBuffer.getClass());
     while (onlyReadBuffer.hasRemaining()){
         System.out.println(onlyReadBuffer.get());
     }
     onlyReadBuffer.put((byte) 10);// ReadOnlyBufferException打开这句,就会抛出异常
    

    }
    }

3.NIO 还提供了MappedByteBuffer,可以让文件直接在内存(堆外的内存)中进行修改,而如何同步到文件由NIO来完成。

import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

/**

  • MappedByteBuffer作用:可以让文件直接在内存(堆外的内存)中进行修改,而操作系统不需要拷贝一次,有点像DirectByteBuffer

  • 实际上DirectByteBuffer也是MappedByteBuffer作用的子类。

  • 注意:执行完代码后,IDEA的文件并没有及时改变,但是如果我们在外面打开文件,他的确是发生改变的。

  • //右键 show in ex... 在打开文件即可
    */
    public class MappedByteBufferTest {
    public static void main(String[] args) throws Exception {

     RandomAccessFile randomAccess = new RandomAccessFile("1.txt", "rw");
     FileChannel fileChannel = randomAccess.getChannel();
     //0-4范围内容可以直接在内存操作 [即五个字节位置]
     /**
      * 第一个参数是 模式, 我们使用的读写模式
      * 第2个参数是  是起始位置
      * 第3个参数是  是映射到内存的大小
      */
     MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
    
     //这里我们就直接在内存操作,就可以修改文件
     mappedByteBuffer.put(0, (byte) 'o');//修改第一个位置
     mappedByteBuffer.put(1, (byte) 'p');//修改第2个位置
     //修改第11个位置, 抛出 IndexOutOfBoundsException,需要将 0,5 改成 0,11 而不是 0,10
     //mappedByteBuffer.put(10, (byte) 'x');
     randomAccess.close();
    

    }
    }

MappedByteBuffer作用:可以让文件直接在内存(堆外的内存)中进行修改,而操作系统不需要拷贝一次,有点像DirectByteBuffer实际上DirectByteBuffer也是MappedByteBuffer作用的子类。
4.前面我们讲的读写操作,都是通过一个Buffer完成的。NIO还支持通过多个Buffer(即Buffer数组)完成读写操作,即Scattering分散和Gathering聚合

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Arrays;

/**

  • Scattering:在将数据写入到buffer中时,可以采用buffer数组,依次写入,一个buffer满了就写下一个。
  • Gatering:在将数据读出到buffer中时,可以采用buffer数组,依次读入,一个buffer满了就读下一个。
    */

/**

  • 使用方式:打开cmd telnet locakhost 8899, 测试一次,就重新启动连接一次 [不足8个,刚刚8个字符,超过8个]
  • 连接后可以输入字符串了
    */

public class ScatteringAndGatherIngTest {
public static void main(String[] args) throws IOException {

    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    InetSocketAddress address = new InetSocketAddress(7000);
    serverSocketChannel.socket().bind(address); //绑定将端口绑定到 scoket ,并启动监听

    int messageLength = 5 + 3;

    ByteBuffer[] byteBuffers = new ByteBuffer[2];
    byteBuffers[0] = ByteBuffer.allocate(5);
    byteBuffers[1] = ByteBuffer.allocate(3);


    SocketChannel socketChannel = serverSocketChannel.accept();
    while (true) {
        int byteRead = 0;
        //接受客户端写入的的字符串
        while (byteRead < messageLength) {
            long r = socketChannel.read(byteBuffers);
            byteRead += r;
            System.out.println("byteRead:" + byteRead);
            //通过流打印
            Arrays.asList(byteBuffers).stream().map(buffer -> "postiton:" +       buffer.position() + ",limit:" + buffer.limit()).forEach(System.out::println);
        }
        
        //将所有buffer都flip。
        Arrays.asList(byteBuffers).forEach(buffer -> {buffer.flip();});
        
        //将数据读出回显到客户端
        long byteWrite = 0;
        while (byteWrite < messageLength) {
            long r = socketChannel.write(byteBuffers);
            byteWrite += r;
        }
        //将所有buffer都clear
        Arrays.asList(byteBuffers).forEach(buffer -> {buffer.clear();});

        System.out.println("byteRead:" + byteRead + ",byteWrite:" + byteWrite + ",messageLength:" + messageLength);
    }
}

}

Selector 选择器
基本介绍
Java的NIO,用非阻塞的IO方式。可以用一个线程,处理多个客户端连接,就会使用到Selector(选择器)
Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
避免了多线程之间的上下文切换导致的开销。

特点在说明:
Netty的IO线程NioEventLoop聚合了Selector(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。
当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。
线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道
由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁I/O阻塞导致的线程被挂起。
一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程模型,架构的性能、弹性伸缩能力和可靠都得到了极大的提升。

Selector类相关方法
Selector类是一个抽象类,常用方法和说明如下:
public abstract class Selector implements Closeable {
public static Selector open();//得到一个选择器对象

public int select(long timeout);//监控所有注册的通道,当其中有 IO 操作可以进行时,将对应的 SelectionKey 加入到内部集合中并返回,参数用来设置超时时间

public Set<SelectionKey> selectedKeys();//从内部集合中得到所有的 SelectionKey	

}

注意事项:
NIO中的ServerSocketChannel功能类似ServerSocket,SocketChannel功能类似Socket。
selector 相关方法说明
selector.select() ; //阻塞
selector.select(1000); //阻塞1000毫秒,在1000毫秒后返回
selector.wakeup(); //唤醒selector
selector.selectNow(); 不阻塞,立马返还

NIO非阻塞 网络编程原理分析图
NIO非阻塞 网络编程相关的(Selector、SelectionKey、ServerScoketChannel和SocketChannel)关系梳理图

对图的说明:
当客户端连接时,会通过ServerSocketChannel得到SocketChannel
Selector进行监听select方法,返回有事件发生的通道的个数。
将SocketChannel注册到selector上,register(Selector sel,int ops),一个selector上可以注册多个SocketChannel
注册返回一个SelectionKe,会和该Selector关联(集合)
进一步得到各个SelectionKey(有时间发生)
在通过SelectionKey反向获取SocketChannel,方法channel()
可以通过得到的channel,完成业务处理

NIO非阻塞 网络编程快速入门

SelectionKey类
SelectionKey,表示Selector和网络通道的注册关系,共四种:
int OP_ACCEPT:有新的网络连接可以 accept,值为 16
int OP_CONNECT:代表连接已经建立,值为 8
int OP_READ:代表读操作,值为 1
int OP_WRITE:代表写操作,值为 4

//源码中:
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;

2.SelectionKey相关方法
public abstract class SelectionKey {
public abstract Selector selector();//得到与之关联的 Selector 对象
public abstract SelectableChannel channel();//得到与之关联的通道
public final Object attachment();//得到与之关联的共享数据
public abstract SelectionKey interestOps(int ops);//设置或改变监听事件
public final boolean isAcceptable();//是否可以 accept
public final boolean isReadable();//是否可以读
public final boolean isWritable();//是否可以写
}

ServerSocketChannel类
ServerSocketChannel 在服务器端监听客户端Socket连接
相关方法如下:
public abstract class ServerSocketChannel extends AbstractSelectableChannel implements NetworkChannel{
public static ServerSocketChannel open(),得到一个 ServerSocketChannel 通道
public final ServerSocketChannel bind(SocketAddress local),设置服务器端端口号
public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
public SocketChannel accept(),接受一个连接,返回代表这个连接的通道对象
public final SelectionKey register(Selector sel, int ops),注册一个选择器并设置监听事件
}

SocketChannel类
SocketChannel,网络IO通道,具体负责进行读写操作。NIO把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。
相关方法如下:
public abstract class SocketChannel extends AbstractSelectableChannel implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel{
public static SocketChannel open();//得到一个 SocketChannel 通道
public final SelectableChannel configureBlocking(boolean block);//设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
public boolean connect(SocketAddress remote);//连接服务器
public boolean finishConnect();//如果上面的方法连接失败,接下来就要通过该方法完成连接操作
public int write(ByteBuffer src);//往通道里写数据
public int read(ByteBuffer dst);//从通道里读数据
public final SelectionKey register(Selector sel, int ops, Object att);//注册一个选择器并设置监听事件,最后一个参数可以设置共享数据
public final void close();//关闭通道
}

posted @ 2020-12-18 20:41  Jacksonwu  阅读(223)  评论(0编辑  收藏  举报