【netty系列-02】深入理解socket本质和BIO底层实现

Netty系列整体栏目


内容链接地址
【一】深入理解网络通信基本原理和tcp/ip协议https://zhenghuisheng.blog.csdn.net/article/details/136359640
【二】深入理解Socket本质和BIOhttps://zhenghuisheng.blog.csdn.net/article/details/136549478
【三】深入理解NIO的基本原理和底层实现https://zhenghuisheng.blog.csdn.net/article/details/138451491

一, Socket本质和初识BIO

在上一篇中,讲解了网络通信的基本原理,以及tcp/ip层与应用层之间的关系,可以得知在 OSI 七层模型中,数据需要先通过应用层将数据转成报文,然后将报文从应用层中传向传输层,封装成报文段,依次将数据封装到网络层,数据链路层,物理层,最后再通过以太网,光纤将数据传到到对应的主机上。

在这里插入图片描述

在网络编程中,由于tcp和ip已经有了对应的协议,因此在tcp层往下只需遵守对应的协议即可,因此在实际开发中,只需要将数据从应用层发送到传输层即可。

因此在操作系统的底层,封装了一个Socket,类似于一个中间件,用于应用层和传输层的TCP/IP协议族之间的通信,该中间层将所有与传输层连接的注意事项全部封装好,让开发者在开发无需关心底层的具体实现,更加的关注业务即可。如一些数据丢包的网络重传,滑动窗口等数据都会提前封装好。socket类似于sqlSession的功能,是一个门面模式,主要用于接收和转发,不做具体的执行功能。

在linux操作系统的源码中,会有一个 socket.c 的文件。在该文件中,里面已经封装了了应用层和tcp协议之间的细节,如如何建立连接,如何接受连接,如何监听,如何绑定等等都已经实现。因此对于网络应用程序来说,只需要与Socket进行交互即可。

1,Socket

客户端发送一条 hello word 到另一个客户端的流程如下,数据从客户端A的应用层再到传输层,再到网络层,再到数据链路层,再到物理层进行层层封包,通过以太网到客户端B的物理层,数据链路层,网络层,传输层,应用层进行层层解析,才能将数据进行解析出来

在这里插入图片描述

对于开发人员来说要实现层层的细节,肯定是不友好的。因此在操作系统底部,就为我们封装了一套socket,内部已经帮我们实现了tcp等协议的细节,让开发者更加的注重于业务上面的开发,其流程可以简化如下

在这里插入图片描述

让开发者只需考虑应用层的业务代码实现,不需要考虑底层的实现细节。因此在网络编程中只需要关注三件事,就是客户端和服务端的连接、读网络数据、写网络数据

2,BIO

2.1,单线程场景

在原生网络编程中,使用BIO编程的比较多,BIO指的是 Blocking IO 阻塞式io,顾名思义,就是在进行io时,会出现阻塞的情况。

先看一段原生通过BIO来实现网络编程的代码,来了解BIO的基本使用和被阻塞的时机,先看一段服务端的代码。改代码中创建一个serverSocket,用于实现应用层和tcp层之间的交互,随后绑定了一个端口8089,当有客户端来访问这个服务的这个端口时,就会做出响应

package com.zhs.netty.bio;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author zhenghuisheng
 * @date 2024/3/7 22:31
 */
public class BioServer {
    public static void main(String[] args) throws IOException {
        //创建一个socket
        ServerSocket serverSocket = new ServerSocket();
        //服务端监听的端口号
        serverSocket.bind(new InetSocketAddress(8089));
        System.out.println("服务端开始监听");
        try{
            while(true){
                //监听事件
                Socket socket = serverSocket.accept();
                try{
                    ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
                    ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
                    //客户端传入的数据
                    String readData = input.readUTF();
                    System.out.println("成功接收到了数据" + readData);
                    output.writeUTF("已经接收到了" + readData);
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    socket.close();
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            serverSocket.close();
        }
    }
}

在启动这个服务端的时候,可以看出有如下信息打印,表示此时正被阻塞着,并且阻塞在这个accept的监听上

服务端开始监听

随后再编写一个客户端的代码。服务端中需要使用ServerSocket,在客户端中则需要使用Socket

package com.zhs.netty.bio;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;

public class BioClient {
    public static void main(String[] args) throws IOException {
        //客户端启动必备
        Socket socket = null;
        //实例化与服务端通信的输入输出流
        ObjectOutputStream output = null;
        ObjectInputStream input = null;
        //服务器的通信地址
        InetSocketAddress addr = new InetSocketAddress("127.0.0.1",8089);
        try{
            socket = new Socket();
            socket.connect(addr);//连接服务器
            System.out.println("连接成功");
            output = new ObjectOutputStream(socket.getOutputStream());
            input = new ObjectInputStream(socket.getInputStream());
            System.out.println("Ready send message.....");
            /*向服务器输出请求*/
            output.writeUTF("zhenghuisheng");
            output.flush();
            //接收服务器的输出
            System.out.println(input.readUTF());
        }finally{
            if (socket!=null) socket.close();
            if (output!=null) output.close();
            if (input!=null) input.close();
        }
    }
}

在启动完客户端之后,可以发现客户端打印的信息如下

连接成功

而在服务端中,由于接收到了客户端的请求,在服务端中也会将阻塞的代码继续往下执行

服务端开始监听
成功接收到了数据zhenghuisheng

除了服务端没有客户端来连接时会阻塞之外,在已经有一个客户端来连接且没释放,再来一个客户端进行连接时,此时的客户端也会出现阻塞的情况,假设在服务端刚开启之后,第一个客户端进行连接时在以下的代码处打一个debug断点阻塞在哪

 output.writeUTF("zhenghuisheng");

此时第二个客户端来建立连接的代码如下,客户端这边不需要绑定具体的端口号,可以直接由操作系统进行分配即可,服务器的通信地址为刚刚设置的ip地址和端口号,目前设置的ip最地址为本地地址

package com.zhs.netty.bio;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;

public class BioClient2 {
    public static void main(String[] args) throws IOException {
        //客户端启动必备
        Socket socket = null;
        //实例化与服务端通信的输入输出流
        ObjectOutputStream output = null;
        ObjectInputStream input = null;
        //服务器的通信地址
        InetSocketAddress addr = new InetSocketAddress("127.0.0.1",8089);
        try{
            socket = new Socket();
            socket.connect(addr);//连接服务器
            System.out.println("连接成功");
            output = new ObjectOutputStream(socket.getOutputStream());
            input = new ObjectInputStream(socket.getInputStream());
            System.out.println("Ready send message.....");
            /*向服务器输出请求*/
            output.writeUTF("zhenghuisheng2号");
            output.flush();
            //接收服务器的输出
            System.out.println(input.readUTF());
        }finally{
            if (socket!=null) socket.close();
            if (output!=null) output.close();
            if (input!=null) input.close();
        }
    }
}

此时客户端2打印的信息如下,就是处于连接成功的状态

连接成功

但是在服务端这边,并不能够感知到第二个服务端来连接,也不能够做出响应。由于双端都是通过socket来进行数据的传输,包括三次握手等等,而客户端2可以连接成功,表示客户端2的socket和服务端的socket已经连接成功了,但是socket是操作系统的资源,由于服务器与一个客户端连接的socket还未释放连接,因此此时的服务端还没有来得及去处理第二个socket,当第一个socket正式的处理完数据传输以及响应,完成四次挥手之后,才可以去处理第二个建立的socket

在这里插入图片描述

因此bio的阻塞就两个地方:

  • 服务端没有接收到客户端请求时会阻塞
  • 已有客户端再进行连接未释放时,新来的客户端连接也会被阻塞

2.2,多线程场景

如果仅仅只是在单线程中用BIO,那么拿过存在多个客户端连接服务端时,那么就会存在没被连接的客户端全部都被阻塞着,此时就是完全变成了串行执行,效率极其低下。但是也可以通过多线程去解决这个问题,每当一个客户端与服务端进行连接时,服务端就开启一个子线程去响应客户端的请求,而在实际开发中,一般都会通过线程池的方式去代替多线程,从而达到线程更好的管理和复用

如下面这段利用线程池的代码,每当一个客户端来进行连接时,就会通过线程池中的线程去执行这些任务

package com.zhs.netty.bio;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ServerPool {
    private static ExecutorService executorService
            = Executors.newFixedThreadPool(
                    Runtime.getRuntime().availableProcessors());

    public static void main(String[] args) throws IOException {
        //服务端启动必备
        ServerSocket serverSocket = new ServerSocket();
        //表示服务端在哪个端口上监听
        serverSocket.bind(new InetSocketAddress(10001));
        System.out.println("Start Server ....");
        try{
            while(true){
                executorService.execute(new ServerTask(serverSocket.accept()));
            }
        }finally {
            serverSocket.close();
        }
    }

    //每个和客户端的通信都会打包成一个任务,交个一个线程来执行
    private static class ServerTask implements Runnable{
        private Socket socket = null;
        public ServerTask(Socket socket){
            this.socket = socket;
        }
        @Override
        public void run() {
            //实例化与客户端通信的输入输出流
            try(ObjectInputStream inputStream =
                    new ObjectInputStream(socket.getInputStream());
                ObjectOutputStream outputStream =
                    new ObjectOutputStream(socket.getOutputStream())){

                //接收客户端的输出,也就是服务器的输入
                String userName = inputStream.readUTF();
                System.out.println("Accept client message:"+userName);

                //服务器的输出,也就是客户端的输入
                outputStream.writeUTF("Hello,"+userName);
                outputStream.flush();
            }catch(Exception e){
                e.printStackTrace();
            }finally {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

但是也会出现一个问题,就是最大的连接数就是和核心线程数以及阻塞队列,核心线程数有关,根据io密集型和cpu密集型去考虑核心线程数的大小,而为了不丢失连接,阻塞队列肯定是越大越好,因此一般这种情况的最大连接数就是核心线程的个数,在一定的并发上会有一定的限制。

并且如果是io密集型的传输,如涉及大文件的io传输这种,那么整体效率就会底下,严重影响客户端的体验

由于BIO会存在着阻塞的缺陷以及并发量小的缺陷,因此随着网络编程的不断发展,BIO这种阻塞的方式使用的频率逐渐变小。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

huisheng_qaq

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值