zoukankan      html  css  js  c++  java
  • 基于JAVA Socket的底层原理分析及工具实现

    前言

     在工作开始之前,我们先来了解一下Socket

      所谓Socket,又被称作套接字,它是一个抽象层,简单来说就是存在于不同平台(os)的公共接口。学过网络的同学可以把它理解为基于传输TCP/IP协议的进一步封装,封装到以至于我们从表面上使用就像对文件流一样的打开、读写和关闭等操作。此外,它是面向应用程序的,应用程序可以通过它发送或接收数据而不用过多的顾及网络协议。

     那么,Socket是存在于不同平台的公共接口又是什么意思呢?  

      形象的说就是“插座”,是不同OS之间进行通信的一种约定或一种方式。通过 Socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。Socket 的典型应用就是 Web 服务器和浏览器,浏览器获取用户输入的 URL,通过解析出服务器的IP地址,向服务器IP发起请求,服务器分析接收到的 URL,将对应的网页内容返回给浏览器,浏览器再经过解析和渲染,就将文字、图片、视频等元素呈现给用户。

     问题又来了,不通过系统之间能否进行Socket通信呢?

      首先,我们了解一下常用操作系统中的Socket。

      在 UNIX/Linux 系统中,为了统一对硬件的操作,简化接口,不同的硬件设备都被看成一个文件。对这些文件的操作,就等同于对磁盘上普通文件的操作。

      你也许听很多高手说过,UNIX/Linux 中的一切都是文件!那个家伙说的没错。

      学过操作系统的同学可能知道,当对文件进行I/O操作时,系统通常会为文件分配一个ID,也就是文件描述符。简单来讲就是系统对文件的操作转化为

    对文件描述符的操作,它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接。

      同样的,网络连接也被定义为是一种类似的I/O操作,类似于文件,它也有文件描述符。

      所以当我们可以通过 Socket来进行一次通信时,也可以被称作操作网络文件的过程。在网络建立时,socket() 的返回值就是文件描述符。有了这个文

    件描述符,我们就可以使用普通的文件操作函数来传输数据了,例如:

    • 用 read() 读取从远程计算机传来的数据;
    • 用 write() 向远程计算机写入数据。

      不难发现,除了不同主机之间的Socket建立过程我们还不清楚,Socket的通信过程就是简单的文件流处理过程。

      在Windows系统中,也有类似“文件描述符”的概念,但通常被称为“文件句柄”。因此,本教程如果涉及 Windows 平台将使用“句柄”,如果涉及Linux

    平台则使用“描述符”。与UNIX/Linux 不同的是,Windows 会区分 socket 和文件,Windows 就把 socket 当做一个网络连接来对待,因此需要调用专们

    针对 socket 而设计的数据传输函数,针对普通文件的输入输出函数就无效了。

     步入正题

      说了这么多,到底不同系统间不同定义的Socket是怎么通信的呢?

      在此,我们以JAVA Socket 与 Linux Socket的关系分析为例进行说明。首先,拿TCP Socket通信过程来讲,就是客户端与服务器进行TCP数据交互,它分为一下几个步骤:

    1. 系统分配资源,服务端开启Socket进程,对特定端口号进行监听
    2. 客户端针对服务端IP进行特定端口的连接
    3. 连接建立,开始通信
    4. 通信完成,关闭连接

      以TCP的通信过程为例,过程如下:

      具体来说,JAVA是怎样完成对底层Linux Socket接口的调用的呢?以下图为例,当我们在JAVA创建一个TCP连接时,需要首先实例化JAVA的ServerSocket类,其中封装了底层的socket()方法、bind()方法、listen()方法。

    其中,socket()方法是JVM对Linux API的调用,详细如下

        1 创建socket结构体
        2 创建tcp_sock结构体,刚创建完的tcp_sock的状态为:TCP_CLOSE
        3 创建文件描述符与socket绑定

      bind ()方法在Linux 的底层详细如下:

        1.将当前网络命名空间名和端口存到bhash()

        可以理解为,绑定到系统能够找到的地方。

      listen()方法在Linux 的底层详细如下:

        1.检查侦听端口是否存在bhash中

        2.初始化csk_accept_queue

        3.将tcp_sock指针存放到listening_hash表

        简单来讲就是验证连接请求的端口是否被开启。

      accpet()方法在Linux 的底层详细如下:  

    1.调用accept方法

    2.创建socket(创建新的准备用于连接客户端的socket)

    3.创建文件描述符

    4.阻塞式等待(csk_accept_queue)获取sock

    我们知道在listen阶段,会为侦听的sock初始化csk_accept_queue,此时这个queue为空,所以accept()方法会在此时阻塞住,直到后面有客户端成功握手后,这个queue才有sock.如果csk_accept_queue不为空,则返回一个sock.后续的逻辑如accept第二个图所示,其步骤如下:

    5.取出sock

    6.socket与sock互相关联

    7.socket与文件描述符关联

    8.将socket返回给线程

      到此,JAVA调用Linux API的初始步骤完成。

      让我们来用JAVA Socket进行简单的编码实现。目标:

      1. 能够实现数据通信

      2. 能够实现客户端和服务端多对一连接。

      在此,以基于TCP连接过程为例,完成以上编码。服务端较为容易实现,它只需要开启监听端口,等待连接。同时对发送和接收模块进行封装,以证上面所说Socket通信相似于IO过程。

    Server端代码:

     1 package tcp_network;
     2 
     3 import java.io.DataInputStream;
     4 import java.io.DataOutputStream;
     5 import java.io.IOException;
     6 import java.net.ServerSocket;
     7 import java.net.Socket;
     8 
     9 public class Tcp_server {
    10     public static void main(String arg[]) throws IOException { 
    11         System.out.print("服务端启动.......
    ");
    12         ServerSocket server = new ServerSocket(9660); //初始化一个监听端口,让系统分配相关socket资源
    13         boolean isRunable = true;
    14         while (isRunable){//循环等待连接的建立
    15             Socket client = server.accept();
    16             System.out.print("一个客户端建立了连接.......
    ");
    17             new Thread(new Channel(client)).start();//每有一个通信连接,将它放到新的线程中去,实现一个服务端对多个客户端
    18         }
    19         server.close();
    20     }
    21     public static class Channel implements Runnable{//封装服务的类,完成接收和发送的实现
    22         private Socket client;
    23         private DataInputStream in_data;
    24         private DataOutputStream out_data;
    25         public Channel(Socket client) throws IOException { //构造函数加载,简单初始化相关输入输出流
    26             this.client = client;
    27             in_data = new DataInputStream(client.getInputStream());//将通信的字节流封装为IO的输入输出流
    28             out_data = new DataOutputStream(client.getOutputStream());
    29         }
    30         public String receive() throws IOException {//通过对输入流
    31             String data = in_data.readUTF();
    32             return  data;
    33         }
    34         public void send(String msg) throws IOException {
    35             out_data.writeUTF(msg);//将数据写到数据流当中
    36             out_data.flush();//刷新缓冲,发送数据
    37         }
    38         public void release() throws IOException {//连接结束时,释放系统资源
    39             in_data.close();
    40             out_data.close();
    41             client.close();
    42         }
    43         @Override
    44         public void run() {
    45             try {
    46                 String receive_data;
    47                 while (true){
    48                     receive_data = receive();
    49                     if(!receive_data.equals(""))
    50                     {
    51                         if(receive_data.equals("Hello"))
    52                         {
    53                             System.out.print("IP:"+client.getInetAddress()+" 客户端信息:"+receive_data+"
    ");
    54                             send("Hi");
    55                         }
    56                         else {
    57                             System.out.print("IP:"+client.getInetAddress()+" 客户端信息:"+receive_data+"
    ");
    58                             send(receive_data.toUpperCase());
    59                         }
    60                     }
    61                 }
    62             } catch (IOException e) {
    63                 try {
    64                     release();
    65                 } catch (IOException ex) {
    66                     ex.printStackTrace();
    67                 }
    68                 e.printStackTrace();
    69             }
    70         }
    71     }
    72 }

    Client端代码:

    package tcp_network;
    
    import java.io.*;
    import java.net.Socket;
    
    public class Tcp_client {
        public static void main(String arg[]) throws IOException {
            Socket client = new Socket("localhost",9660);//新建socket资源
            boolean isRuning = true;
            while (isRuning) {
                //new Send(client).send();
                //new Receive(client).receive();
                new Thread(new Send(client)).start();//启动发送线程
                new Thread(new Receive(client)).start();//启动接收线程
            }
        }
        public static class Send implements Runnable
        {
            private DataOutputStream out_data;
            private BufferedReader console;
            private String msg;
            public Send(Socket client) throws IOException {
                this.console = new BufferedReader(new InputStreamReader(System.in));//接收系统输入
                this.msg = init();
                try {
                    this.out_data = new DataOutputStream(client.getOutputStream());//将字符流转化为数据流
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
            private String init() throws IOException {
                String msg=console.readLine();
                return msg;
            }
            @Override
            public void run() {//在线程体内实现发送数据
                try {
                    out_data.writeUTF(msg);
                    out_data.flush();
                    System.out.println("send date !");
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
        public static class Receive implements Runnable{//将接收模块单独封装,目的是避免通信时接收一直阻塞
            private DataInputStream in_data;
            private String msg;
            public Receive(Socket client){
                try{
                    in_data = new DataInputStream(client.getInputStream());//转换流
    
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            @Override
            public void run() {
                String data = null;
                try {//在线程中实现接收  IO缓冲区数据并输出
                    data = in_data.readUTF();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                System.out.print("服务端:"+data+"
    ");
            }
        }
    
    }

     注意:在客户端需要把发送和接收模块放到两个线程中去,否则会出现客户端一直阻塞等待接收,不能进行下次发送数据的情况(解决办法:放到不同线程中接收发送能够互不影响)。

     效果如下:

        

      

    参考:

      https://blog.csdn.net/vipshop_fin_dev/article/details/102966081

      http://c.biancheng.net/view/2128.html

  • 相关阅读:
    Codeforces 1255B Fridge Lockers
    Codeforces 1255A Changing Volume
    Codeforces 1255A Changing Volume
    leetcode 112. 路径总和
    leetcode 129. 求根到叶子节点数字之和
    leetcode 404. 左叶子之和
    leetcode 104. 二叉树的最大深度
    leetcode 235. 二叉搜索树的最近公共祖先
    450. Delete Node in a BST
    树的c++实现--建立一棵树
  • 原文地址:https://www.cnblogs.com/xshun/p/11991482.html
Copyright © 2011-2022 走看看