网络编程和socket

一、网络编程

0. 和web应用的区别

  • 网络编程:模拟计算机通信中运输层之间的对等通信,忽略下层的打包和拆包,直接在运输层建立一条虚连接进行数据传输。
  • 网络编程主要是指网络通信,实现计算机之间的对话和文件传输等,如,QQ、P2P点对点通信等;而web主要就是B/S结构的应用,通俗一点说就是开发网站、网页,如,QQ空间、百度。
  • 而web应用还停留在应用层,且并没有建立虚连接直接在对等层间通信,程序准备好数据后依然要利用计算机自己的底下四层来传输数据。
  • web开发几乎用不到java网络编程。因为web项目都是运行在tomcat等容器里,容器帮我们封装了网络编程和多线程这一块,这也是web开发比较容易的原因。
  • web开发用不到、也几乎怒可能自己写网络编程,因为http协议有httpclient等第三方封装,TCP/UDP有nettty第三方框架。

1. 简介

  • 网络编程的实质就是两个或多个计算机之间的数据传输。
  • 和普通的单机程序相比,网络程序最大的不同就是需要交换数据的程序运行在不同的计算机。
  • 虽然通过IP地址和端口可以找到网络上运行的一个程序,但是如果需要进行网络编程,则还需要了解网络通讯的过程。

2. 网络通讯

  • 网络通讯基于请求-响应模型,类似一答一问,也就是通讯的一端发送数据,另一端则反馈数据。

  • C/S结构:第一次主动发起通讯的程序叫做客户,而在第一次通讯中等待连接的程序叫做服务器。

  • 网络编程中的两种程序就分别是客户(客户端指运行客户程序的机器)和服务器(服务器端指运行服务器程序的机器),比如QQ程序,每个QQ用户安装的是QQ客户端程序,而QQ服务器端程序则运行在腾讯公司的机房中,为大量的QQ用户提供服务。这种网络编程结构成为客户端/服务器结构,也叫作C/S结构。

  • 使用C/S结构的程序,在开发时需要分别开发客户端和服务端。优势在于客户端是专门开发的,可以根据需要实现各种效果,但是通用性差,一种程序的客户端只能和对应的服务器端通讯,而不能和其他服务器端通讯,而如果客户端用浏览器代替,那么浏览器可以同时访问微信的服务端也可以访问QQ邮箱的服务端,这就是区别。

  • B/S结构:使用通用客户端(如浏览器)的结构叫做浏览器/服务器结构,简称B/S,是一种特殊的C/S结构。

  • 在开发时只需开发服务器端即可(客户端只需要写个图形界面?),但是浏览器的闲置比较大,表现力不强,无法进行系统级操作。

  • 协议:网络编程中最重要的概念。由于网络编程时运行在不同计算机中两个程序之间的数据交换,所以为了让接收端理解该数据,需要规定该数据的格式,这个数据的格式就是协议。

3. 网络通讯方式

  • 网络通讯方式主要有两种:
  1. TCP方式(传输控制协议)
  2. UDP方式(用户数据报协议)
  • TCP方式类似于拨打电话,UDP方式就类似于发送短信,既不建立连接,也不重传,因此接收方可能接收不到。
  • 重要的数据一般使用TCP方式进行数据传输,而大量的非核心数据则通过UDP方式传输。

4. 网络编程步骤

  • 下面以C/S结构为基础介绍网络编程步骤,当然,B/S结构只需要实现服务端即可。

4.1 客户端网络编程步骤

  • 客户端一般实现程序界面和基本逻辑。这里我们介绍使用TCP协议的客户端编程,主要由三个步骤实现:
  1. 建立网络连接
  • 在建立网络连接时需要指定连接到的服务器IP和端口号,建立完成以后,会形成一条虚拟的连接,后续的操作就可以通过该连接实现数据交换了。也就是模拟计算机通信中运输层的功能,它在使用TCP协议传送数据之前,必须先建立一条虚连接,虚连接指的是这并非是一条真正的物理连接,TCP报文段先要传送到IP层,加上IP首部后,再传送到数据链路层。

  • 在Java中,和网络编程有关的Api位于java.net中,Socket类则是其中的一个基础类,它即是用来建立连接的类。

  • Java对于TCP方式的网络编程提供了良好的支持,Socket类代表客户端连接,ServerSocket类代表服务端连接。

  • 建立连接也就是创建Socket类型的对象,带对象代表网络连接。

          Socket socket1 = new Socket(“192.168.1.103”,10000); //IP地址+端口
          Socket socket2 = new Socket(“www.sohu.com”,80);
    
  • 连接到某台主机的某个端口,如果建立连接时本机网络不通,或服务器端程序未开启,则会抛出异常。

  1. 交换数据
  • 通过建立的链接交换数据,必须严格按照请求-响应模型进行,由客户端发送一个请求数据到服务器,服务器反馈一个响应数据给客户端。

  • Java中数据传输功能由Java IO实现,也就是说只要从链接中获得输入流和输出流即可。

      OutputStream os = socket1.getOutputStream(); //获得输出流
      InputStream is = socket1.getInputStream(); //获得输入流
    
  • 后续的操作就变成了IO操作。我们可以先向输出流中写入数据,这些数据会被系统发送出去,然后再从输入流中读取服务器的反馈信息,这样就完成了一次数据交换过程。

  1. 关闭网络连接
  • 释放程序占用的端口、内存等系统资源,结束网络编程。

            socket1.close();
    
  • 示例:一个简单的网络客户端程序示例,作用是向服务器端发送一个字符串“Hello”,并将服务器端的反馈显示到控制台。不过现在还不能正常运行,需要和等下的服务端同步运行才能监听到10000.

      public static void main(String[] args) {
          Socket socket=null;
          InputStream is=null;
          OutputStream os=null;
    
          //服务器ip地址
          String serverIp="127.0.0.1";
          //服务器端端口号
          int port=10000;
          //发送内容
          String data="Hello";
    
          try {
              //建立连接
              socket=new Socket(serverIp,port);
              //发送数据
              os=socket.getOutputStream();
              os.write(data.getBytes());
              //接收反馈数据
              //输入流的获取可以在连接建立后任一步,不一定非要在发送完数据后
              is=socket.getInputStream(); 
              byte[] b=new byte[1024];
              int n=is.read(b);
              System.out.println("服务器反馈: "+new String(b,0,n));
          } catch (IOException e) {
              e.printStackTrace();
          } finally{
              try {
                  //关闭流和链接
                  is.close();
                  os.close();
                  socket.close();
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
          
      }
    

4.2 服务器端网络编程步骤

  • 服务器端一般实现程序的核心逻辑和数据库存储等核心功能。
  1. 监听端口
  • 这个端口就是服务器开放给客户端的端口,也就是监听是否有客户端连接到达。

      //实现服务器监听
      ServerSocket ss = new ServerSocket(10000);
    
  • 如果10000端口已被占用,则抛出异常。

  1. 获得连接
  • 获得当前到达的服务器的客户端连接。

      Socket socket=ss.accept();
    
  • 接下来和客户端交换数据就通过这个连接了。

  1. 交换数据
  • 服务器的数据交换步骤和客户端的相反,是先接收客户端发送过来的数据,然后进行逻辑处理,再把结果发送给客户端。
  1. 关闭连接
  • 当服务器程序关闭时,关闭服务端连接。

      ss.close();
    
  • 实例:实现一个echo服务器,echo的意思是回声,就是将客户端发送的内容原封不动地反馈给客户端。先运行该程序,再运行客户端。

      public static void main(String[] args) {
          ServerSocket serverSocket=null;
          Socket socket=null;
          OutputStream os=null;
          InputStream is=null;
    
          //监听端口号
          int port=10000;
    
          try {
              //建立连接
              serverSocket=new ServerSocket(port);
              //获得客户端连接;该方法是阻塞方法,进入这一步的执行时会等待客户端
              socket=serverSocket.accept(); 
              //接收客户端发送内容
              is=socket.getInputStream();
              byte[] b=new byte[1024];
              int n=is.read(b);
              System.out.println("客户端发送内容:"+new String(b,0,n));
              //反馈
              os=socket.getOutputStream();
              os.write(b,0,n);
          } catch (IOException e) {
              e.printStackTrace();
          } finally {
              try {
                  //关闭流和连接
                  is.close();
                  os.close();
                  socket.close();
                  serverSocket.close();
              } catch (IOException e) {
                  e.printStackTrace();
              }
    
          }
      }
    

5. 如何使一个服务器支持多个客户端同时连接

  • 实际的服务器需要同时支持多个客户端工作,我们可以通过多线程来实现。

  • 实际上服务端的accept()获得当前客户端的连接后,即释放了当前客户端对该端口的占用,服务器可以继续监听其他连接的到来,但如果只有一个线程,那么处理完当前请求程序也就结束了,端口自然也没人监听了,所以必须结合循环和线程才能实现继续监听并处理下一个连接的功能。

  • MulThreadSocketServer:在主程序中监听端口,然后设置一个循环来无限次地接收和处理请求,接收请求的操作是公共的,而处理操作的请求是各异的,需要开一个线程来分开执行。

      public class MultiThreadSocketServer {
          public static void main(String[] args) {
              ServerSocket serverSocket=null;
              Socket socket=null;
              int port=10000;
      
              try {
                  //监听端口
                  serverSocket=new ServerSocket(port);
                  System.out.println("服务器已启动:");
                  while (true){
                      System.out.println("收到一个请求...");
                      //获得连接
                      socket=serverSocket.accept();
                      //开线程处理请求
                      new LogicThread(socket);
                  }
              } catch (IOException e) {
                  e.printStackTrace();
              } finally {
                  try {
                      //关闭连接
                      serverSocket.close();
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
      
          }
      }
    
      /**
       * @Author haien
       * @Description 用于处理请求的线程
       * @Date 2019/6/26
       **/
      public class LogicThread extends Thread {
          Socket socket;
          InputStream is;
          OutputStream os;
      
          public LogicThread(Socket socket) {
              this.socket = socket;
              //启动线程
              start();
          }
      
          @Override
          public void run() {
              byte[] b=new byte[1024];
              try {
                  os=socket.getOutputStream();
                  is=socket.getInputStream();
                  //进行多次数据交换,客户端应该相应地也发出这么多次数据
                  for (int i=0; i<3; i++){
                      //接收数据
                      int n=is.read(b);
                      //处理数据
                      byte[] response=logic(b,0,n);
                      //反馈
                      os.write(response);
                  }
              } catch (IOException e) {
                  e.printStackTrace();
              } finally {
                  //关闭流和连接
                  close();
              }
      
          }
      
          /**
           * @Author haien
           * @Description 处理数据,实际就是直接拷贝
           * @Date 2019/6/26
           * @Param [b, start, len]
           * @return byte[]
           **/
          public byte[] logic(byte[] b,int start,int len){
              byte[] response=new byte[len];
              //直接拷贝数组
              System.arraycopy(b,0,response,0,len);
              return response;
          }
      
          /**
           * @Author haien
           * @Description 关闭流和连接
           * @Date 2019/6/26
           * @Param []
           * @return void
           **/
          public void close(){
              try {
                  is.close();
                  os.close();
                  socket.close();
              } catch (IOException e) {
      
              }
          }
      }
    
  • 从LogicThread可以看出,该服务器匹配进行三次数据交换的客户端。

  • MulSocketClient:发起三次数据交换。

      public class MulSocketClient {
          public static void main(String[] args) {
      
              Socket socket = null;
              InputStream is = null;
              OutputStream os = null;
              String serverIP = "127.0.0.1";
              int port = 10000;
              //发送内容
              String data[] ={"First","Second","Third"};
      
              try {
                  //建立连接
                  socket = new Socket(serverIP,port);
                  os = socket.getOutputStream();
                  is = socket.getInputStream();
                  byte[] b = new byte[1024];
      
                  for(int i = 0;i < data.length;i++){
                      //发送数据
                      os.write(data[i].getBytes());
                      //接收数据
                      int n = is.read(b);
                      //输出反馈数据
                      System.out.println("服务器反馈:" + new String(b,0,n));
                  }
      
              } catch (Exception e) {
                  e.printStackTrace(); //打印异常信息
              }finally{
                  try {
                      //关闭流和连接
                      is.close();
                      os.close();
                      socket.close();
                  } catch (Exception e2) {}
      
              }
      
          }
      }
    
  • MulSocketClient2:第二个客户端,把发送数据改一下就行。

  • 先执行服务端程序,再执行两个客户端,可以看到两个客户端都收到了反馈。

  • 但是客户端和服务器的数据交换次数还不够灵活,不应该卡死在三次,如果客户端的次数不固定怎么办呢?我们可以使用某个特殊的字符串,表示客户端退出,不过这就涉及到网络协议的内容了。

  • 在实际的服务器中,由于硬件和端口数的限制,不能无限制地创建线程对象,实际上频繁地创建线程对象效率也比较低,所以程序中都实现了线程池来提高程序的执行效率。

  • 线程池:Thread pool,是池基数的一种,就是在程序启动时首先把所需数目的线程对象创建好,然后当客户端连接到达时从池中取出一个线程对象来使用。当客户端连接关闭以后,将该线程对象重新放入线程池中供其他客户端复用,这样可以提供程序的执行速度,优化程序对于内存的占用等。

5. 网络协议

  • 实际就是如何解析接收到的数据的一种约定,服务端如何理解客户端发来的数据呢,客户端又如何解析服务端反馈的数据呢?协议就做了规定,对于程序实现来说,也就是一些判断、选择、处理代码了,比如,判断客户端发来的数字是否为0,为0则反馈一个-1表示参数异常,而客户端接收到反馈则给它们分类,1是成功,-1是失败。

  • 参考文章(https://blog.csdn.net/sihai12345/article/details/79334299)

  • 代码实例:ideaProjects/jar-test/socket

socket

在网络编程的应用

  • 两台计算机之间用什么传输数据呢?首先需要物理连接,如今大部分计算机已经连接到互联网,因此不用担心这一点。
  • 在此基础上,只要考虑如何编写数据传输程序,但实际上这点也不用愁,因为操作系统已经提供了socket。

简介

  • 问题抛出:我们每天打开浏览器浏览网页时,浏览器的进程怎么与web服务器通信的?当你用QQ聊天时,QQ进程怎么与服务器或你好友所在的QQ进程通信?
  • 这些都得靠socket。
  • socket的原意是插座,在计算机通信领域翻译为套接字,它是计算机之间进行通信的一种约定或一种方式。
  • 通过这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。
  • socket的典型应用就是web服务器和浏览器:浏览器获取用户输入的url,向服务器发起请求,服务器分析接收到的url,将对应的网页内容返回给浏览器,浏览器再经过解析和渲染,就将文字、图片、视频等元素呈现给用户。
  • 在计算机网络中,TCP连接的端点就是socket,它由IP地址+端口号组成,用来确定一台计算机中的某一应用程序。
  • 在UNIX/Linux中,为了简化接口,不同的硬件设备也都被看成一个文件,socket就是一种特殊的文件。对这些文件的操作,等同于对磁盘上普通文件的操作。
  • 这和UNIX/Linux的思想不谋而合,计算机中的一切都是文件。
  • 对于文件,都可以用打开-读写-关闭的模式来操作,看上面网络编程的步骤就知道了。

你可能感兴趣的:(网络编程和socket)