Socket通信原理及模型实现

本文涉及的产品
数据传输服务 DTS,数据迁移 small 3个月
推荐场景:
MySQL数据库上云
简介: Socket通信原理及模型实现

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/网络分层/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

相关实践学习
RocketMQ一站式入门使用
从源码编译、部署broker、部署namesrv,使用java客户端首发消息等一站式入门RocketMQ。
Sqoop 企业级大数据迁移方案实战
Sqoop是一个用于在Hadoop和关系数据库服务器之间传输数据的工具。它用于从关系数据库(如MySQL,Oracle)导入数据到Hadoop HDFS,并从Hadoop文件系统导出到关系数据库。 本课程主要讲解了Sqoop的设计思想及原理、部署安装及配置、详细具体的使用方法技巧与实操案例、企业级任务管理等。结合日常工作实践,培养解决实际问题的能力。本课程由黑马程序员提供。
相关文章
|
2月前
|
数据处理 C# C++
如何使用C#和C++结构体实现Socket通信
如何使用C#和C++结构体实现Socket通信
|
4月前
|
网络协议 程序员 Python
揭秘Python网络编程:深入理解Socket通信
在当今信息时代,网络通信已经成为人们生活中不可或缺的一部分。而Python作为一种高效、易用的编程语言,自然也能够很好地支持网络编程和Socket通信。本文将介绍Python网络编程与Socket通信的相关知识,包括Socket通信模型、Socket编程接口、网络套接字等内容。
|
4月前
|
网络协议 开发者 Python
Python网络编程与Socket通信:连接世界的无限可能
在当今数字化时代,Python作为一种强大的编程语言,通过网络编程与Socket通信为我们打开了连接世界的无限可能。本文将深入探讨Python网络编程的基础知识、Socket通信的原理以及实际应用,帮助读者更好地理解并运用这一技术。
|
4月前
|
网络协议
【Netty 网络通信】Socket 通信原理
【1月更文挑战第9天】【Netty 网络通信】Socket 通信原理
|
6月前
|
存储 Cloud Native Linux
C++封装了socket通信类
C++封装了socket通信类
|
7月前
|
存储
14.10 Socket 套接字选择通信
对于网络通信中的服务端来说,显然不可能是一对一的,我们所希望的是服务端启用一份则可以选择性的与特定一个客户端通信,而当不需要与客户端通信时,则只需要将该套接字挂到链表中存储并等待后续操作,套接字服务端通过多线程实现存储套接字和选择通信,可以提高服务端的并发性能,使其能够同时处理多个客户端的请求。在实际应用场景中,这种技术被广泛应用于网络编程、互联网应用等领域。在服务端启动时,创建套接字并进行绑定,然后开启一个线程(称为主线程)用于监听客户端的连接请求。主线程在接收到新的连接请求后,会将对应的套接字加入一个数据结构(例如链表、队列、哈希表等)中进行存储。同时,主线程会将存储套接字的数据结构传递给
25 1
14.10 Socket 套接字选择通信
|
7月前
14.5 Socket 应用组播通信
组播通信是一种基于UDP协议的网络通信方式,它允许发送方将消息同时传递给多个接收方。在组播通信中,发送方和接收方都会加入一个共同的组播组,这个组播组对应一个特定的IP地址,所有加入该组播组的主机都能够接收到发送方发送的消息。组播通信可以有效地减少网络流量和网络负载,因为在传统的点对点通信方式下,每个消息都需要单独传输到每个接收方,而在组播通信中,每个消息只需要传输一次,就可以同时传递给多个接收方。在使用组播模式时,需要在套接字上使用`setsockopt()`函数来设置套接字的`IP_MULTICAST_IF`选项,指定本地主机的出站接口地址,用于发送组播数据包。此外,还可以设置`IP_ADD
72 0
14.5 Socket 应用组播通信
|
17天前
|
存储 网络协议 关系型数据库
Python从入门到精通:2.3.2数据库操作与网络编程——学习socket编程,实现简单的TCP/UDP通信
Python从入门到精通:2.3.2数据库操作与网络编程——学习socket编程,实现简单的TCP/UDP通信
|
1月前
|
网络协议 Unix Linux
Socket通信详细介绍1
Socket通信详细介绍
34 0