Socket通信原理及模型实现

本文涉及的产品
数据传输服务 DTS,数据同步 small 3个月
推荐场景:
数据库上云
数据传输服务 DTS,数据迁移 small 3个月
推荐场景:
MySQL数据库上云
数据传输服务 DTS,数据同步 1个月
简介: 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

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
Sqoop 企业级大数据迁移方案实战
Sqoop是一个用于在Hadoop和关系数据库服务器之间传输数据的工具。它用于从关系数据库(如MySQL,Oracle)导入数据到Hadoop HDFS,并从Hadoop文件系统导出到关系数据库。 本课程主要讲解了Sqoop的设计思想及原理、部署安装及配置、详细具体的使用方法技巧与实操案例、企业级任务管理等。结合日常工作实践,培养解决实际问题的能力。本课程由黑马程序员提供。
相关文章
|
4月前
|
缓存 监控 Java
Java Socket编程最佳实践:优化客户端-服务器通信性能
【6月更文挑战第21天】Java Socket编程优化涉及识别性能瓶颈,如网络延迟和CPU计算。使用非阻塞I/O(NIO)和多路复用技术提升并发处理能力,减少线程上下文切换。缓存利用可减少I/O操作,异步I/O(AIO)进一步提高效率。持续监控系统性能是关键。通过实践这些策略,开发者能构建高效稳定的通信系统。
140 1
|
2月前
|
Python
python socket 简单通信
python socket 简单通信
37 1
|
2月前
|
网络协议 安全 网络安全
网络编程:基于socket的TCP/IP通信。
网络编程:基于socket的TCP/IP通信。
142 0
|
4月前
|
Java 数据挖掘 开发者
Java网络编程进阶:Socket通信的高级特性与应用
【6月更文挑战第21天】Java Socket通信是分布式应用的基础,涉及高级特性如多路复用(Selector)和零拷贝,提升效率与响应速度。结合NIO和AIO,适用于高并发场景如游戏服务器和实时数据分析。示例展示了基于NIO的多路复用服务器实现。随着技术发展,WebSockets、HTTP/2、QUIC等新协议正变革网络通信,掌握Socket高级特性为应对未来挑战准备。
41 1
|
4月前
|
Java
Java Socket编程与多线程:提升客户端-服务器通信的并发性能
【6月更文挑战第21天】Java网络编程中,Socket结合多线程提升并发性能,服务器对每个客户端连接启动新线程处理,如示例所示,实现每个客户端的独立操作。多线程利用多核处理器能力,避免串行等待,提升响应速度。防止死锁需减少共享资源,统一锁定顺序,使用超时和重试策略。使用synchronized、ReentrantLock等维持数据一致性。多线程带来性能提升的同时,也伴随复杂性和挑战。
81 0
|
4月前
|
安全 Java 网络安全
Java Socket编程教程:构建安全可靠的客户端-服务器通信
【6月更文挑战第21天】构建安全的Java Socket通信涉及SSL/TLS加密、异常处理和重连策略。示例中,`SecureServer`使用SSLServerSocketFactory创建加密连接,而`ReliableClient`展示异常捕获与自动重连。理解安全意识,如防数据截获和中间人攻击,是首要步骤。通过良好的编程实践,确保网络应用在复杂环境中稳定且安全。
82 0
|
21天前
|
网络协议 Linux 应用服务中间件
Socket通信之网络协议基本原理
【9月更文挑战第14天】网络协议是机器间交流的约定格式,确保信息准确传达。主要模型有OSI七层与TCP/IP模型,通过分层简化复杂网络环境。IP地址全局定位设备,MAC地址则在本地网络中定位。网络分层后,数据包层层封装,经由不同层次协议处理,最终通过Socket系统调用在应用层解析和响应。
|
2月前
|
网络协议 Linux 应用服务中间件
Socket通信之网络协议基本原理
【8月更文挑战第27天】网络协议定义了机器间通信的标准格式,确保信息准确无损地传输。主要分为两种模型:OSI七层模型与TCP/IP模型。
|
2月前
|
网络协议
socket编程(2) -- TCP通信
socket编程(2) -- TCP通信
33 0
|
4月前
|
Java API 开发者
Java网络编程基础与Socket通信实战
Java网络编程基础与Socket通信实战
下一篇
无影云桌面