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

  • 相关阅读:
    洛谷P1071 潜伏者
    2019BJFU 网站设计(孙俏-web前端开发)实验代码-181002222
    反思——P1307 数字反转
    洛谷P1067 多项式输出
    湖南大学第十五届程序设计竞赛(重现赛)
    2019河北省大学生程序设计竞赛(重现赛)
    2019BJFU C++实验习题(完结)
    配置android source 在ubuntu中编译环境
    Android屏幕保持唤醒状态
    Android richtext
  • 原文地址:https://www.cnblogs.com/xshun/p/11991482.html
Copyright © 2011-2022 走看看