Socket通信原理及模型实现

5 篇文章 0 订阅
4 篇文章 0 订阅

1. 网络分层模型

在这里插入图片描述

因特网协议栈共有五层:应用层、传输层、网络层、链路层、物理层。不同于OSI七层模型这也是实际使用中使用的分层方式。

  • 应用层
    支持网络应用,应用协议仅仅是网络应用的一个组成部分,运行在不同主机上的进程则使用应用层协议进行通信。主要的协议有:http、ftp、telnet、smtp、pop3等。
  • 传输层
    负责为信源和信宿提供应用程序进程间的数据传输服务,这一层上主要定义了两个传输协议,传输控制协议即TCP和用户数据报协议UDP。
  • 网络层
    负责将数据报独立地从信源发送到信宿,主要解决路由选择、拥塞控制和网络互联等问题。
  • 链路层
    负责将IP数据报封装成合适在物理网络上传输的帧格式并传输,或将从物理网络接收到的帧解封,取出IP数据报交给网络层。
  • 物理层
    负责将比特流在结点间传输,即负责物理传输。该层的协议既与链路有关也与传输介质有关。

网络分层模型优点:

  • 分层结构将应用系统正交地划分为若干层,每一层只解决问题的一部分,通过各层的协作提供整体解决方案。大的问题被分解为一系列相对独立的子问题,局部化在每一层中,这样就有效的降低了单个问题的规模和复杂度,实现了复杂系统的第一步也是最为关键的一步分解。
  • 分层结构具有良好的可扩展性,为应用系统的演化增长提供了一个灵活的框架,具有良好的可扩展性。增加新的功能时,无须对现有的代码做修改,业务逻辑可以得到最大限度的重用。同时,层与层之间可以方便地插入新的层来扩展应用。
  • 分层架构易于维护。在对系统进行分解后,不同的功能被封装在不同的层中,层与层之间的耦合显著降低。因此在修改某个层的代码时,只要不涉及层与层之间的接口,就不会对其他层造成严重影响。

2. Socket概念

套接字(Socket) 是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:

  • 连接使用的协议
  • 本地主机的IP地址
  • 本地进程的协议端口
  • 远地主机的IP地址
  • 远地进程的协议端口
    在这里插入图片描述
    Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去封装复杂的传输层协议(TCP/IP)并承载应用层数据格式协议(http\telnet\ftp)完成服务间数据传输交互。
    在这里插入图片描述

一个Socket是一对IP地址和端口。Socket可以看成在两个程序进行通讯连接中的一个端点,一个程序将一段信息写入Socket中,该Socket将这段信息发送给另外一个Socket中,使这段信息能传送到其他程序中。你可以这么理解:socket是进程之间用来对话的中间层工具。

3. Socket作用

在这里插入图片描述
应用层通过传输层进行数据通信时,TCP和UDP会遇到同时为多个应用程序进程提供并发服务的问题。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了称为套接字(Socket)的接口,区分不同应用程序进程间的网络通信和连接。

  • Socket(套接字) 维护的是一对服务ip、端口之间的映射关系,通过ip、端口能够溯源到通信双方的信息。
  • Socket本身并不是协议 Http协议是处于应用层的数据传输组装方式,而TCP/IP协议是处于传输层的数据组装方式,在网络分层模型中独立分隔各自在对应的层级中发挥作用。
  • Socket 是对TCP/IP 或者UDP/IP协议封装的调用接口(API), 是协调应用层和传输层的交互的通道和工具,通过这个API程序员在开发网络应用程序的时候,就可以不用关心底层是怎么实现的,减轻开发的难度。

4. Socket、Http、TCP/IP的关系

名称含义网络模型层级作用
http、ftp、telnet数据文本格式协议应用层解决如何包装数据
socket套接字应用层<=>传输层封装TCP/IP传输层协议,连接的抽象,对应用层使用传输层提供API支持
TCP/IP传输通信协议传输层服务数据传输交互
  • 创建Socket连接时,可以指定使用的传输层协议,Socket可以支持不同的传输层协议(TCP或UDP),当使用TCP协议进行连接时,该Socket连接就是一个TCP连接。
  • 我们在传输数据时,可以只使用(传输层)TCP/IP协议,但是那样的话,如 果没有应用层,便无法识别数据内容,如果想要使传输的数据有意义,则必须使用到应用层协议,应用层协议有很多,比如HTTP、FTP、TELNET等,也 可以自己定义应用层协议。WEB使用HTTP协议作应用层协议,以封装HTTP文本信息,然后使用TCP/IP做传输层协议将它发到网络上。

5. Socket交互

在这里插入图片描述
服务端(Server)

  • socket() socket初始化
  • listen() 开启服务端口监听
  • accept() 等待Client连接
  • read() 读取Client发送的数据
  • write() 向Client发送数据
  • close() 关闭socket连接

客户端(Client)

  • socket() socket初始化
  • read() 读取Server发送的数据
  • write() 向Server发送数据
  • close() 关闭socket连接

6. Socket模型实现

所谓模型实现,可以泛泛认为是网络IO模型的实现,Socket可以视为一个网络IO交互的通道,是数据载体的抽象,也就是说输入(Input)、输出(Output)数据最终都是作为数据流通过Socket架起的桥梁进行服务间通信的,因此这里通过IO模型配合Socket进行论述。

6.1 单线程 BIO 模型

在这里插入图片描述

单线程BIO模型不能支持并发请求,同一时刻只能对一个服务请求进行响应,其他服务请求将会被阻塞,而且在数据也是阻塞读,所有的服务请求都是顺序执行的,这是单线程BIO模型的缺点。

缺点

  • accept()方法 调用accept方法后意味着阻塞等待服务连接请求,一旦有服务请求连接则不再接收其他服务请求的连接。当一次服务请求结束后需要再次调用accept方法才可以再次接收服务请求。因此一般会进行while轮询,上一次处理完随即调用accept将服务状态就绪,等待新服务连接的请求。
  • read()方法 调用read方法后意味着在Socket连接中对服务连接方的输入流进行读取,此时方法是阻塞读取的,无论此时是否有对端数据输入进来,方法会一直阻塞。因此在此场景下会迟迟无法再次调用accept方法,其他新服务请求也就无法连接。

以下是单线程 BIO Server代码:

/**
 * 每次只能处理一个连接请求,其他连接请求会被阻塞
 * created by guanjian on 2021/1/12 9:09
 */
public class SingleThreadBIOSocketServer {

    private static byte[] bytes = new byte[1024];

    public static void main(String[] args) throws IOException, InterruptedException {
        ServerSocket socketServer = new ServerSocket();
        socketServer.bind(new InetSocketAddress(9999));

        System.out.println("Server starting ...");

        while (true) {

            System.out.println("Server waiting connect ...");
            //accept方法会阻塞
            Socket socket = socketServer.accept();

            long start = System.currentTimeMillis();
            System.out.println("Server begin receive data ...");
            //read方法会阻塞
            socket.getInputStream().read(bytes);
            long end = System.currentTimeMillis();
            System.out.format("Server finish receive data: %s , cost: %s \n", new String(bytes), end - start);
        }
    }
}

以下是Client代码:

/**
 * 单独一个socket请求
 * created by guanjian on 2021/1/12 9:09
 */
public class SingleThreadNIOSocketClient {

    public static void main(String[] args) throws IOException {
        SocketChannel socketClient = SocketChannel.open();
        socketClient.connect(new InetSocketAddress("127.0.0.1", 9999));

        System.out.println("Client connected ...");

        Scanner scanner = new Scanner(System.in);

        while (true){
            //会阻塞socket等待输入
            String input = scanner.next();

            if ("close".equals(input)){
                socketClient.close();
                return;
            }
            socketClient.write(ByteBuffer.wrap(input.getBytes()));

            System.out.format("Client sending content:%s \n", input);
        }

    }
}

6.2 多线程 BIO 模型

在这里插入图片描述
针对单线程 BIO 模型进行优化,将单线程处理服务连接请求改为多线程并行处理,解决了accept阻塞问题,但是read仍然是阻塞读

  • 优点
    ①通过多线程接收服务请求,即每次accept收到连接请求随即开启线程处理,之后立刻调用accept接收新服务请求,accept阻塞问题得到解决
    ②由于一个线程对应一个连接请求,服务请求之前互不影响,Socket 1 出现阻塞和异常不会影响Socket 2
  • 缺点
    当连接量巨大时,使用大量线程资源处理服务请求撑爆服务器,比如双十一大促场景

Server代码如下:

**
 * 开启多线程来接收处理Socket请求
 * 不会被阻塞socket请求,但是会耗费大量线程资源
 *
 * created by guanjian on 2021/1/12 9:09
 */
public class MultiThreadBIOSocketServer {

    private static byte[] bytes = new byte[1024];

    public static void main(String[] args) throws IOException, InterruptedException {

        ServerSocket socketServer = new ServerSocket();
        socketServer.bind(new InetSocketAddress(9999));
        System.out.println("Server starting ...");

        while (true) {
            System.out.println("Server waiting connect ...");
            Socket socket = socketServer.accept();
            //接收请求后开启线程来处理,避免accept阻塞
            if (null != socket) {
                new SockectServerHandler(socket).start();
            }
        }
    }

    static class SockectServerHandler extends Thread {
        private Socket socket;

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

        @Override
        public void run() {
            try {
                long start = System.currentTimeMillis();
                System.out.println("Server begin receive data ...");
                //read方法会阻塞
                socket.getInputStream().read(bytes);
                long end = System.currentTimeMillis();
                System.out.format("Server finish receive data: %s , cost: %s \n", new String(bytes), end - start);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

6.3 单线程 NIO 模型

在这里插入图片描述
尽管多线程BIO通过多线程解决了并发处理的问题,但是read仍然是阻塞读,且accept连接依然是阻塞等待,也存在资源占用浪费的问题。下面通过JDK的NIO包提供的NIO模型来进一步解决accept、read阻塞问题。

  • 优点
    ①ServerSocketChannel是SocketServer的NIO版本,支持accept函数阻塞、非阻塞控制,通过configureBlocking进行配置,解决accept阻塞等待耗费资源的问题
    ②SocketChannel是Socket的NIO版本,支持read函数阻塞、非阻塞控制,通过configureBlocking进行配置,解决read阻塞等待耗费资源的问题
  • 缺点
    ①需要维护SocketChannel,遍历检查是否有新的连接(accept)到达,且检查是否有新的数据流(read)输入
    ②由于是遍历操作,当SocketChannel集合非常巨大时会成有性能问题,需要及时处理的Channel无法立刻进行处理,比如当前有10万个channel保持连接,遍历需要从1到10万依次遍历,而可能第10万个Channel此时需要处理,而大部分时间和性能都耗费在前10万-1个Channel的遍历和轮询上
    ③遍历channel每次进行read都会经历用户态、内核态切换

Client可以使用SocketChannel这种NIO模型,也可以使用之前介绍的Socket这种BIO模型,主要的是Server要使用NIO模型。Server代码如下:

/**
 * 单线程处理
 * created by guanjian on 2021/1/12 9:09
 */
public class SingleThreadNIOSocketChannelServer {

    /**
     * 存储SocketChannel集合
     */
    private static List<SocketChannel> channels = Lists.newArrayList();

    public static void main(String[] args) throws IOException, InterruptedException {
        ServerSocketChannel socketServer = ServerSocketChannel.open();
        socketServer.socket().bind(new InetSocketAddress(9999));
        //设置非阻塞模式,这里控制的是accept函数是否阻塞
        socketServer.configureBlocking(false);

        System.out.println("Server starting ...");

        while (true) {
            System.out.println("Server waiting connect ...");
            SocketChannel socketChannel = socketServer.accept();
            if (null != socketChannel) {
                System.out.println("Server receive connect ...");
                //设置非阻塞模式,这里控制的是read函数是否阻塞
                socketChannel.configureBlocking(false);
                channels.add(socketChannel);
            }

            System.out.format("============ channels:%s ============\n", channels.size());
            Optional.ofNullable(channels).ifPresent(channels->{
                channels.stream().filter(channel -> channel.isConnected()).forEach(channel -> {
                    try {
                        System.out.println("Server read data begin ...");

                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

                        int len = channel.read(byteBuffer);
                        if (len > 0) {
                            byteBuffer.flip();
                            System.out.format("Server receive data : %s ...\n", byteBufferToString(byteBuffer));
                        } else {
                            System.out.println("Server receive data is empty ...");
                        }
                        System.out.println("Server read data end ...");
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });
            });
            //为了打印日志,故意设置时间间隔
            Thread.sleep(2000);
        }
    }

    public static String byteBufferToString(ByteBuffer buffer) {
        CharBuffer charBuffer = null;
        try {
            Charset charset = Charset.forName("UTF-8");
            CharsetDecoder decoder = charset.newDecoder();
            charBuffer = decoder.decode(buffer);
            return charBuffer.toString();
        } catch (Exception ex) {
            ex.printStackTrace();
            return null;
        }
    }
}

6.4 单线程 NIO 多路复用 模型

在这里插入图片描述
在JDK的NIO模型中,除了上述解决阻塞问题外,还提供了强大的选择器功能,可以做到多路复用的效果。选择器是基于事件驱动的,相比于遍历自身维护的Channel集合,Selector选择器的select函数会依赖系统底层提供epoll函数一次性获取到监听的channel文件描述符,因此无需遍历所有channel。

  • 优点:
    依赖系统底层事件驱动函数epoll一次性获取所关心触发监听事件的socketChannel,时间复杂度从全局遍历的O(n) 降低到 O(1)
  • 注意:
    由于依赖操作系统底层API支持,因此不同OS效果不同,Linux下实现了epoll函数
  • 总结:
    如上所述,Socket是对底层协议的封装,所有的API也都是依赖底层的映射和封装

Client代码可以复用以上,Server代码如下:

/**
 * 单线程处理 多路复用
 * created by guanjian on 2021/1/12 9:09
 */
public class SingleThreadNIOSocketChannelSelectorServer {

    public static void main(String[] args) throws IOException, InterruptedException {
        // 创建ServerSocketChannel通道,绑定监听端口为8080
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(9999));
        // 设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        // 注册选择器,设置选择器选择的操作类型
        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("Server starting ...");

        while (true) {
            System.out.println("Server receive request ...");
            // 等待请求,每次等待阻塞3s,超过时间则向下执行,若传入0或不传值,则在接收到请求前一直阻塞
            if (selector.select(1000) > 0) {
                System.out.println("Server receive event ...");
                // 获取待处理的选择键集合
                Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
                while (keyIterator.hasNext()) {
                    SelectionKey selectionKey = keyIterator.next();

                    // 如果是连接请求,调用处理器的连接处理方法
                    if (selectionKey.isAcceptable()) {
                        System.out.println("Server receive connect ...");
                        handleAccept(selectionKey);
                    }
                    // 如果是读请求,调用对应的读方法
                    if (selectionKey.isReadable()) {
                        System.out.println("Server receive read ...");
                        handleRead(selectionKey);
                    }
                    // 处理完毕从待处理集合移除该选择键
                	keyIterator.remove();
                }
            }
            //为了打印日志,故意设置时间间隔
            Thread.sleep(2000);
        }

    }

    public static void handleAccept(SelectionKey selectionKey) throws IOException {
        SocketChannel socketChannel = ((ServerSocketChannel) selectionKey.channel()).accept();
        socketChannel.configureBlocking(false);
        socketChannel.register(selectionKey.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(1024));
    }

    public static void handleRead(SelectionKey selectionKey) throws IOException {
        // 获取套接字通道
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        // 获取缓冲器并进行重置,selectionKey.attachment()为获取选择器键的附加对象
        ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
        byteBuffer.clear();
        // 没有内容则关闭通道
        if (socketChannel.read(byteBuffer) == -1) {
            socketChannel.close();
        } else {
            // 将缓冲器转换为读状态
            byteBuffer.flip();
            // 将缓冲器中接收到的值按localCharset格式编码保存
            String receivedRequestData = Charset.forName("UTF-8").newDecoder().decode(byteBuffer).toString();
            System.out.format("Server receive data:" + receivedRequestData);
            // 关闭通道
            //socketChannel.close();
        }
    }
}

6.5 性能对比

模型线程资源IO类型优点缺点
单线程 BIO 模型单线程阻塞IO线程资源消耗小不能支持并发
多线程 BIO 模型多线程阻塞IO支持并发线程资源消耗大
单线程 NIO 模型单线程非阻塞IO①线程消耗资源小
②接收请求(accept)非阻塞、读取数据流(read)非阻塞
需遍历socket,时间复杂度O(n)
单线程 NIO 多路复用 模型单线程非阻塞IO①线程消耗资源小
②接收请求(accept)非阻塞、读取数据流(read)非阻塞
③事件驱动,获取socket时间复杂度O(1)
依赖OS底层支持和实现

7. 参考

https://www.bilibili.com/video/BV1Sh411o77h
https://www.bilibili.com/video/BV1E54y1i7pA
https://baike.baidu.com/item/%E7%BD%91%E7%BB%9C%E5%88%86%E5%B1%82/659207
https://www.cnblogs.com/pipci/p/12527394.html
https://www.cnblogs.com/wangcq/p/3520400.html
https://www.cnblogs.com/yinyu/p/5298916.html
https://blog.csdn.net/phoenix_cat/article/details/84595505
https://www.cnblogs.com/lxyit/p/9209407.html
https://blog.csdn.net/ycgslh/article/details/79604074
https://www.cnblogs.com/chenss15060100790/p/9368959.html

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
Socket编程是一种通信机制,用于在网络实现不同计算机之间的数据传输。它基于客户端-服务器模型,其中一个程序充当服务器,等待其他程序(客户端)连接并发送数据。 Socket编程的原理如下: 1. 创建Socket:服务器Socket通过调用socket()函数创建一个套接字,用于监听和接受客户端连接。客户端Socket也通过调用socket()函数创建一个套接字,用于向服务器发送连接请求。 2. 绑定Socket:服务器Socket使用bind()函数将其套接字与特定的IP地址和端口号绑定。这样当客户端Socket连接时,可以将数据发送到正确的目标。 3. 监听和接受连接:服务器Socket使用listen()函数开始监听来自客户端的连接请求。一旦收到连接请求,服务器Socket使用accept()函数接受连接,并创建一个新的套接字来处理与该客户端的通信。 4. 连接服务器:客户端Socket使用connect()函数向服务器发出连接请求。如果连接成功,客户端Socket将与服务器Socket建立一个通信通道。 5. 数据传输:一旦连接建立,服务器和客户端之间可以通过send()和recv()函数进行数据传输。send()函数用于将数据发送给另一方,recv()函数用于接收对方发送的数据。 6. 关闭连接:当通信完成后,可以使用close()函数关闭套接字,释放资源,并终止连接。 这是Socket编程的基本原理,通过套接字的创建、绑定、监听、连接和数据传输等步骤,可以实现不同计算机之间的通信。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

大摩羯先生

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

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

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

打赏作者

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

抵扣说明:

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

余额充值