ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

【Java基础】网络编程-TCP编程01

2020-01-27 15:01:22  阅读:177  来源: 互联网

标签:01 Java socket 编程 端口 服务端 连接 Socket 客户端


文章目录

Socket编程概念

Java的网络编程主要涉及到的内容是Socket编程,那么什么是Socket呢?

  1. Socket通常称作“套接字”就是两台主机之间逻辑连接的端点。,通常通过“套接字”向网络发出请求或者响应网络请求。
  2. TPC/IP协议传输层协议,主要解决数据如何在网络中传输,而HTTP应用层协议,主要解决如何包装数据。Socket,本质上就是一组接口,是对TCP/IP协议的封装和应用(程序员层面上)。
  3. 使用Socket进行网络编程时,本质上就是两个进程之间的网络通信。其中一个进程必须充当服务端,它会主动监听某个指定的端口, 另一个进程必须充当客户端,它必须主动连接服务端的IP地址和指定端口如果连接成功,服务端和客户端就成功地建立了一个TCP连接,双方后续就可以随时发送和接收数据。
    因此,当Socket连接成功地在服务器端和客户端之间建立后:
    1. 对服务端来说,它的Socket是指定的IP地址和指定的端口号
    2. 对客户端来说,它的Socket是它所在计算机的IP地址一个由操作系统分配的随机端口号

Socket通信步骤

Socket编程主要涉及到客户端服务端两个方面:

  1. 首先是在服务器端创建一个服务器套接字(ServerSocket),并把它附加到一个端口上,服务器从这个端口监听连接。端口号的范围是0到65536,但是0到1024是为特权服务保留的端口号,我们可以选择任意一个当前没有被其他进程使用的端口。

  2. 客户端请求与服务器进行连接的时候,根据服务器的域名或者IP地址,加上端口号,打开一个套接字。当服务器接受连接后,服务器和客户端之间的通信就像输入输出流一样进行操作`。

TCP通信能实现两台计算机之间的数据交互,通信的两端要严格区分为客户端(Client)服务端(Server)

两端通信时步骤:

  1. 服务端程序需要事先启动,等待客户端的连接。
  2. 客户端主动连接服务器端,连接成功才能通信。服务端不可以主动连接客户端。
  3. 客户端和服务端会建立一个逻辑连接,连接中包含一个对象即IO对象,于是客户端和服务端就可以使用IO对象进行通信,通信的数据不仅仅是字符,所以IO对象是字节流对象.

在这里插入图片描述

  • 客户端和服务端进行一次交互,需要4个IO流对象

  • 服务器端和客户端进行交互时,必须明确两件事:

    1. 多个客户端同时和服务器进行交互,服务器必须明确和哪个客户端进行交互。在服务器端有一个方法,叫accept,可以获取到客户端的请求对象。
    2. 多个客户端和服务器进行交互,就需要使用多个IO流对象。这里要注意的是,服务器是没有IO流的,服务器可以获取到请求的客户端对象Socket,使用每个客户端Socket中提供的IO流和客户端进行交互。服务器使用客户端的字节输出流获取客户端发送的数据。服务器使用客户端的字节输出流给客户端回写数据。
      简单记忆:服务器使用客户端的流和客户端进行交互。 举个例子,就像我请你吃饭,但是没带钱,我向你借钱然后请你吃饭一样。

socket编程相关类

Socket、TCP和部分IP的功能都是由操作系统提供的,不同的编程语言只是提供了对操作系统调用的简单的封装。例如,Java提供的几个Socket相关的类就封装了操作系统提供的接口。

在Java中,提供了两个类用于实现TCP通信程序:

  • 客户端java.net.Socket类表示。创建Socket对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信。
  • 服务端java.net.ServerSocket类表示。创建ServerSocket对象,相当于开启一个服务,并等待客户端的连接。

Socket类

  • 该类实现客户端套接字,套接字指的是两台设备之间通讯的端点。
Socket构造方法
方法 描述
public Socket() 构造一个Socket,因为没有指定目标主机和端口,所以不会通过网络进行连接。
public Socket(String host, int port) 通过一个主机和端口构建一个Socket。构造Socket的时候会连接目标主机,如果连接不到目标主机则会抛出IOException或UnknownHostException异常。
public Socket(Proxy proxy) 通过一个代理构建一个未连接的Socket
public Socket(InetAddress address, int port) 通过一个InetAddress 和端口构建一个Socket,构造的时候也会进行连接目标主机
public Socket(String host, int port, InetAddress localAddr, int localPort) 通过一个要连接的远程主机和端口,并指定从本地哪个ip和端口连接
public Socket(InetAddress address, int port, InetAddress localAddr, int localPort) 通过一个要连接的远程主机和端口,并指定从本地哪个ip和端口连接

小贴士:回送地址(127.x.x.x) 是本机回送地址(Loopback Address),主要用于网络软件测试以及本地机进程间通信,无论什么程序,一旦使用回送地址发送数据,立即返回,不进行任何网络传输。

Socket常用方法
方法 描述
close() 关闭此socke,一旦一个socket被关闭,它不可再使用。关闭此socket也将关闭相关的InputStream和OutputStream
connect(SocketAddress endpoint) 将此socket 连接到服务器,结合socket无参构造方法使用
connect(SocketAddress endpoint, int timeout) 将此socket 连接到服务器,并指定一个超时值,结合socket无参构造方法使用
getInputStream() 返回此socket 的输入流,如果此Scoket具有相关联的通道,则生成的InputStream 的所有操作也关联该通道,关闭生成的InputStream也将关闭相关的Socket。
getOutputStream() 返回此socket 的输出流,如果此Scoket具有相关联的通道,则生成的OutputStream 的所有操作也关联该通道。关闭生成的OutputStream也将关闭相关的Socket
shutdownOutput() 禁用此套接字的输出流=>任何先前写出的数据将被发送,随后终止输出流
getRemoteSocketAddress() 返回此socket连接到的远程地址,返回一个SocketAddress 对象
InetAddress getInetAddress() 返回此socket连接到的远程IP,返回一个InetAddress 对象
getPort() 返回此socket连接到的远程端口
getLocalSocketAddress() 返回此socket绑定的的本地地址,返回一个SocketAddress 对象
getLocalAddress() 返回此socket连接到的的本地IP,返回一个InetAddress 对象
getLocalPort() 返回此socket 绑定到的本地端口
connect方法

connet放提供了两个方法,一个是传入SocketAddress 进行连接目标地址,另一个是通过SocketAddress 和超时等待时间来连接目标地址。

  • connect(SocketAddress endpoint)
  • connect(SocketAddress endpoint, int timeout)
    Socket socket = new Socket();
    SocketAddress socketAddr = new InetSocketAddress("www.baidu.com", 80);
    socket.connect(socketAddr);
    //socket.connect(socketAddr, 2000);
    
    代码中使用了Socket的空构造函数进行构造Socket对象。空构造不会进行连接目标主机(因为没有设置目标地址),需要使用connet方法进行连接目标主机。
代理服务器

使用public Socket(Proxy proxy)构造方法来构造一个使用proxy的socket。以后使用该socket的相关的网络操作都会通过代理服务器进行连接。

//构造代理服务器地址
SocketAddress sa = new InetSocketAddress("192.168.10.130", 808);
//构造Socket代理
Proxy proxy = new Proxy(Proxy.Type.SOCKS, sa);
//使用代理创建socket
Socket socket = new Socket(proxy);
//构造目标地址
SocketAddress socketAddr = new InetSocketAddress("www.baidu.com", 80);
//socket使用代理连接目标地址
socket.connect(socketAddr);
半关闭连接

如果想关闭socket的输入或输出则可以使用一下两个方法。

//当调用shutdownInput()时,则不允许再次从socket中读取数据。
public void shutdownInput() //关闭输入流
//当调用shutdownOutPut()方法后会告诉流已经输入完成,不允许再次输入。
//对方读取流时,会接受到流结束标志(会返回-1)。
public void shutdownOutput()//关闭输出流

关闭输入或输出不会关闭socket的,因为他们不会释放本地端口,还需要调用socket的close()方法来关闭socket。

//下面两个方法来判断socket的输入或输出流是否关闭。
public boolean isInputShutdown()//是否关闭输入流
public boolean isOutputShutdown()//是否关闭输出流

如果socket未连接(通过空构造方法构造的socket),也会返回false(未关闭状态)

判断socket是否关闭
//是否连接过目标地址
//isConnected方法并不是连接状态才返回true,而是只要连接过目标地址就返回true,哪怕已经关闭的socket也会返回true的。
public boolean isConnected()
//是否关闭过socket
//isClosed方法是判断socket是否调用close()方法关闭过socket。
public boolean isClosed()

如果使用空构造方法构建socket而不连接目标主机,还没调用close方法,该方法会返回true。所以要判断该socket是否连接目标地址需要这样判断

//打开过连接,但还没有关闭连接
if(socket.isConnected() && !socket.isClosed()){
     System.out.println("连接状态");
}else{
     System.out.println("未打开连接或已经关闭连接");
}
设置Socket属性的相关方法
方法 描述
setReuseAddress(boolean on) 启用/禁用 SO_REUSEADDR 套接字选项。
setSoTimeout(int timeout) 通过指定超时值启用/禁用 SO_TIMEOUT,以毫秒为单位。
1. 该选项表示accept()方法等待客户端连接的时间,当accept()方法 在超时时间内没有获得连接,就会抛出SocketTImeoutException。
setReceiveBufferSize(int size) 为从此 ServerSocket 接受的套接字的 SO_RCVBUF 选项设置默认建议值。
1.设置缓冲大小,和Socket的该选项一样

setReuseAddress()

  1. 和Socket的setReuseAddress(boolean on)方法一样,当网络上仍然有旧数据向ServerSocket传输,是否允许新的ServerSocket绑定到与旧的ServerSocket 同样的端口上,在某些操作系统上允许重用端口,有些则不允许。
  2. 很多服务器程序都是用固定的端口,当程序关闭后,端口可能还被占用一段时间,如果此时立刻重启服务器,服务器就会无法绑定端口,抛出BindException异常
  3. 为了确保不发生这种异常,就可以调用ServerSocket的setReuseAddress(boolean on)方法
    if(!serverSocket.getResuseAddress())  {
    	serverSocket.setReuseAddress(boolean on);
    }
    

ServerSocket类

  • 该类实现了服务器套接字,该对象等待通过网络的请求
ServerSocket构造方法
方法 描述
public ServerSocket() 构造一个ServerSocket,但不绑定任何端口,所以也不能接受任何的请求连接。以后可以通过·bind()方法来进行绑定。
public ServerSocket(int port) 通过一个端口来构造一个ServerSocket对象。默认的tcp队列大小为50,默认监听本地所有的ip地址(如果有多个网卡)
public ServerSocket(int port, int backlog) 通过一个端口和TCP队列大小来构造一个ServerSocket对象。默认监听本地所有的ip地址(如果有多个网卡)。
public ServerSocket(int port, int backlog, InetAddress bindAddr) 通过一个端口、TCP队列大小和一个InetAddress 来构造一个ServerSocket对象。

一个服务器可能有多个网卡,多个ip地址,通过此构造传入一个InetAddress ,可以只监听从此网卡过来的请求连接。
比如:一个电脑上有两个网卡,一个是外网地址,一个是内网地址。为了安全此ServerSocket只允许监听内网ip地址的请求,而不接收外网请求。

  1. TCP分为全连接队列和半连接队列。这里说的是全连接队列
  2. 半连接队列:第一次握手,服务器收到客户端的请求时,该请求连接放到半连接队列中。
  3. 全连接队列:已经通过三次握手后,把当前连接信息存放到全连接队列中。全连接队列中的连接等待ServerSocket.accpt()处理。
  1. 上面参数中port为端口号InteAddress为IP地址backlog表示请求队列的长度,backlog的作用是: 当有一个Socket向服务器发出连接请求时,此连接请求进入请求队列,当请求队列满,而又有Socket对象发出连接请求时,此连接会被拒绝,客户端抛出ConnectException。

  2. 除了第一种不带参数的构造方法外,其他构造方法都使是服务器与特定端口绑定。如果无法绑定到对应端口,会抛出BindException,一般端口被占用,或者操作系统不允许普通用户绑定1~1023之间的端口,就会造成此异常。

  3. 如果把port设置为0,则端口由操作系统随机分配,这种端口称为匿名端口,通常不会使用匿名端口,因为客户端需要明确服务器的端口才可以向服务器发出连接请求。

  4. 和Socket类一样,对于有多个IP的主机,我们采用第四种构造方法来设置项用的IP;
    第(1)种构造方法存在的意义是,有的服务器选项需要在绑定端口之前设置,这个时候就需要先创建ServerSocket对象, 再设置选项,最后再绑定端口,如以下例子:

	  ServerSocket serverSocket = new SernerSocket();
	  serverSocket.serReuseAddress(true);   //设置ServerSocket选项
	  serverSocket.bind(new InteSocketAddress(8000));   //绑定端口

如果把上面代码改成下面这样,则这个选项设置无效

	 ServerSocket serverSocket = new SernerSocket(8000);
	 serverSocket.serReuseAddress(true);
ServerSocket常用方法
方法 描述
public Socket accept() 该方法会从全连接队列中获取一个客户端Socket请求。该方法是阻塞方法。如果当前没有请求的连接,则会一直阻塞,直到有客户端连接请求为止。
bind(SocketAddress endpoint)
bind(SocketAddress endpoint, int backlog)
通过无参构造的ServerSocket对象,需要bing方法进行绑定操作才能处理客户端的请求。
1.通过SocketAddress 进行绑定,默认TCP队列大小为50。
2.通过SocketAddress 和 TCP请求队列大小 两个参数 进行绑定。
InetAddress getInetAddress() 获取本地地址,如果本地有多个ip,随机返回一个
int getLocalPort() 获取绑定的端口,如果构造ServerSocket中默认端口传入一个0,则是随机生成一个端口,这时就需要使用此方法来获取端口信息。
SocketAddress getLocalSocketAddress() 返回本地地址和端口
close() 调用该方法使服务器释放端口,并断开所有与客户端之间的连接。当一个服务器运行结束时,即使没有执行该方法,也会释放端口,因此服务器并不一定要在结束之前执行close()方法。
accpt()方法
  • 该方法从连接请求队列中获取一个客户端请求,然后创建与客户端连接的Socket对象,并返回。
  • 如果队列中没有连接请求,此方法会一直等待下去,直到收到连接请求才返回,之后服务器从Socket对象中获取输入流和输出流和客户端交换数据,
  • 如果发送数据时客户端断开连接,服务器会抛出SocketException异常,通信完成后,应在finally块应关闭Socket对象,释放资源。
使用ServerSocket 判断当前系统已经占用的端口
public static void main(String[] args) throws Exception {
    for(int port=1; port<65535; port++){
        try{
            ServerSocket s = new ServerSocket(port);
        }catch(IOException e){
            System.out.println("当前系统中已经使用的端口:"+port);
        }
    }
}

在这里插入图片描述

Socket编程步骤

InetAddress用来描述主机地址;
Socket用来创建两台主机之间的连接;
ServerSocket用来侦听来自客户端的请求;
Socket通常称作“套接字”,通常通过“套接字”向网络发出请求或者应答网络请求

  1. 服务端:创建ServerSocket对象,绑定监听端口
  2. 服务端:通过accept()方法监听客户端请求
  3. 客户端:创建Socket对象,指明需要连接的服务器的地址和端口号
  4. 客户端:连接建立后,通过输出流向服务器发送请求信息
  5. 服务端:连接建立后,通过输入流读取客户端发送的请求信息
  6. 服务端:通过输出流向客户端发送响应信息
  7. 客户端:通过输入流获取服务器相应的信息
  • 注意:客户端、服务器端都使用Socket中的getInputStream方法和getOutputStream方法获得输入流和输出流,进一步进行数据读写操作

创建服务端

public class SocketServer {
private static String CHARSET = "utf-8";

    public static void main(String[] args) {
        BufferedReader br = null;
        BufferedWriter bw = null;

        try {
            ServerSocket serverSocket = new ServerSocket(8080);
            System.out.println("服务端初始化。。。。。");

            //设置ServerSocket超时表示5秒内没有客户端与服务端已经建立连接,则会抛出异常
            //serverSocket.setSoTimeout(5000);
            
            // 监听客户端请求
            Socket socket = serverSocket.accept();

            //放在这里表示客户端与服务端已经建立连接,5秒内客户端没有发起请求,则会抛出异常
            //socket.setSoTimeout(5000);

            //服务器端都使用Socket中的getInputStream方法和getOutputStream方法获得输入流和输出流,进一步进行数据读写操作
            br = new BufferedReader(new InputStreamReader(socket.getInputStream(), CHARSET));
            bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), CHARSET));

            while (true) {
                String request = br.readLine();//read()和readLine()都会读取对端发送过来的数据,如果无数据可读,就会阻塞直到有数据可读。或者到达流的末尾,这个时候分别返回-1和null。
                System.out.println("服务端接收请求=>" + request);

                System.out.print("服务端响应=>");
                Scanner scanner = new Scanner(System.in);
                String response = scanner.nextLine();
                bw.write(response);
                bw.newLine();
                bw.flush();

                if(response.equals("exit")){
                    System.out.println("服务端关闭连接");
                    socket.close();
                    serverSocket.close();
                    break;
                }

                /*System.out.println("服务端:isInputShutdown="+socket.isInputShutdown());
                System.out.println("服务端:isOutputShutdown="+socket.isOutputShutdown());
                System.out.println("服务端:isBound="+socket.isBound());
                System.out.println("服务端:isConnected="+socket.isConnected());
                System.out.println("服务端:isClosed="+socket.isClosed());*/
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //这里不能随便关闭流,否则会把socket也关闭了(因为后面还要发送数据,所以不能关闭流,不管是关闭输入输入其中之一,都会导致输入和输出都不能使用)
            // 我这里是因为使用了循环,不手动结束循环不会走finally
            try {
                if (bw != null) {
                    bw.close();
                }
                if (br != null) {
                    br.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }
}

创建客户端

public class SocketClient {
 private static String HOST = "127.0.0.1";
    private static int PROT = 8080;
    private static String CHARSET = "utf-8";


    public static void main(String[] args) {
        BufferedWriter bw = null;
        BufferedReader br =  null;

        try {
            Socket clientSocket = new Socket(HOST, PROT);
            System.out.println("客户端初始化。。。。。");

            //放在这里表示5秒内客户端与服务端已经建立连接,但5秒内没有读取到服务端响应,则会抛出异常
            //clientSocket.setSoTimeout(5000);

            // 获取连接服务端的输入输出流,用于向服务器提交数据或者获取响应
             bw = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream(), CHARSET));
             br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), CHARSET));

             // 在一次连接中循环与服务端进行交互
            while (true) {
                //客户端发起请求
                System.out.print("客户端发起请求=>");
                Scanner scanner = new Scanner(System.in);
                String request = scanner.nextLine();

                bw.write(request);
                //因为在服务端使用的是readLine,所以如果不调用newLine,那么会一直阻塞
                bw.newLine();
                bw.flush();

				//客户端单向断开与服务端的输出流,关闭之后客户端不能再向服务端进行输出
                //clientSocket.shutdownOutput();
                String reqponse = br.readLine();// read()和readLine()都会读取对端发送过来的数据,如果无数据可读,就会阻塞直到有数据可读。或者到达流的末尾,这个时候分别返回-1和null。
                System.out.println("客户端接受响应=>"+reqponse);

                if(request.equals("exit")){
                    System.out.println("客户端关闭连接");
          			clientSocket.close();
                    break;
                }

                /*System.out.println("客户端:isInputShutdown="+clientSocket.isInputShutdown());
                System.out.println("客户端:isOutputShutdown="+clientSocket.isOutputShutdown());
                System.out.println("客户端:isBound="+clientSocket.isBound());
                System.out.println("客户端:isConnected="+clientSocket.isConnected());
                System.out.println("客户端:isClosed="+clientSocket.isClosed());*/
            }

        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //这里不能随便关闭流,否则会把socket也关闭了(因为后面还要发送数据,所以不能关闭流,不管是关闭输入输入其中之一,都会导致输入和输出都不能使用)
            // 我这里是因为使用了循环,不手动结束循环不会走finally
            try {
                if (bw != null) {
                    bw.close();
                }
                if (br != null) {
                    br.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

小结,使用readLine()一定要注意:

  1. 读入的数据要注意有/r或/n或/r/n
  2. 没有数据时会阻塞,在数据流异常或断开时才会返回null
  3. 使用socket之类的数据流时,要避免使用readLine(),以免为了等待一个换行/回车符而一直阻塞

Java中Socket上的Read操作阻塞问题
从上面的实例代码中可以看出read()是一个阻塞方法

  • read()和readLine()都会读取对端发送过来的数据,如果无数据可读,就会阻塞直到有数据可读。或者到达流的末尾,这个时候分别返回-1和null。这个特性使得编程非常方便也很高效。

但是这样也有一个问题,就是如何让程序从这两个方法的阻塞调用中返回。

  1. 发送方发送完数据后调用Socket的shutdownOutput()方法关闭输出流,这样对端的输入流上的read操作就会返回-1

    注意: 不能调用socket.getInputStream().close(),这样会导致socket被关闭
    当然如果不需要继续在socket上进行读操作,也可以直接关闭socket。但是这个方法不能用于通信双方需要多次交互的情况。

    out.write("sender say hello socket".getBytes());
    out.flush();
    client.shutdownOutput();  //调用shutdown 通知对端请求完毕
    

    这个解决方案缺点非常明显,socket任意一端都依赖于对方调用shutdownOutput()来完成read返回 -1,如果任意一方没有执行shutdown函数那么就会出现问题。所以一般我们都会在socket请求时设置连接的超时时间 socket.setSoTimeout(5000)以防止长时间没有响应造成系统瘫痪。

  2. 为了防止read操作造成程序永久挂起,还可以给socket设置超时。 (根据我的经验,只有在Socket级别设置才有效)

    如果read()方法在设置时间内没有读取到数据,就会抛出一个java.net.SocketTimeoutException异常。
    例如下面的方法设定超时3秒 : socket.setSoTimeout(3000);

    while (true) {
       server = serverSocket.accept();
       System.out.println("server socket is start……");
       server.setSoTimeout(5000);
       //     .....
     }
    
  3. 发送数据时,约定数据的首部固定字节数为数据长度。这样读取到这个长度的数据后,就不继续调用read方法

    这种方式优点是不依赖对方调用shutdown方法响应较快缺点数据传输的最大字节数固定,需双方事先约定好长度,伸缩性差

  4. 发送数据时,双方约定结尾字符信息,在读取到相应信息时,客户端主动发送断开连接的信息,或者发送信号给服务端,由服务端断开连接。

    如: 约定前几位返回数据byte[]长度大小或最后输出 \n 或 \r 作为数据传输终止符。

    客户端

    
    	out.write("sender say hello socket \n".getBytes());
    	out.flush();
    

    服务器端

    	//服务端
    	byte[] buf = new byte[1];
        int size = 0;
        StringBuffer sb = new StringBuffer();
        while (( size = in.read(buf,0,buf.length)) != -1) {
            String str = new String(buf);
            if(str.equals("\n")) {
                break;
            }
            sb.append(str);
            System.out.print(str);
        }
    			
    

这种方式是对第3种方案的改良版,但不得不说 这是目前socket数据传输的最常用处理read()阻塞的解决方案。


服务器和客户端之间交互时使用BufferedReader的阻塞问题
Socket java.net.SocketException: Connection reset的解决方案
java socket多线程通讯,解决read阻塞问题
通过设置标记为解决基于tcp协议的socket通信阻塞问题


Java Socket学习—多线程阻塞
java基于tcp协议的Socket通信(多线程)
Java使用多线程实现Socket多客户端的通信-my2
Java Socket TCP编程(Server端多线程处理)
java多线程实现多客户端socket通信
TCP的三次握手与四次挥手理解及面试题(很全面)
理解TCP/IP三次握手与四次挥手的正确姿势
TCP-三次握手和四次挥手简单理解

oollXianluo 发布了65 篇原创文章 · 获赞 110 · 访问量 5483 私信 关注

标签:01,Java,socket,编程,端口,服务端,连接,Socket,客户端
来源: https://blog.csdn.net/qq877728715/article/details/104061551

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有