基于JAVA Socket的底层原理分析及工具实现

前言

 在工做开始以前,咱们先来了解一下Sockethtml

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

 那么,Socket是存在于不一样平台的公共接口又是什么意思呢?  浏览器

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

 问题又来了,不经过系统之间可否进行Socket通讯呢?

  首先,咱们了解一下经常使用操做系统中的Socket。网络

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

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

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

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

  一样的,网络链接也被定义为是一种相似的I/O操做,相似于文件,它也有文件描述符。this

  因此当咱们能够经过 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("服务端启动.......\n"); 12         ServerSocket server = new ServerSocket(9660); //初始化一个监听端口,让系统分配相关socket资源 13         boolean isRunable = true; 14         while (isRunable){//循环等待链接的创建 15             Socket client = server.accept(); 16             System.out.print("一个客户端创建了链接.......\n"); 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+"\n"); 54                             send("Hi"); 55  } 56                         else { 57                             System.out.print("IP:"+client.getInetAddress()+" 客户端信息:"+receive_data+"\n"); 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+"\n"); } } }

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

 效果以下:

    

 

 

 

  

参考:

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

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

相关文章
相关标签/搜索