IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> 【网络】网络编程 -> 正文阅读

[系统运维]【网络】网络编程

网络编程

基本概念

1??网络编程:通过代码实现两个进程间的网络通信(这两个进程可以是同一主机上的,也可以是不同主机上的)

进程具有隔离性,每个进程都有自己独立的虚拟地址空间,要想实现进程间通信,需要借助一个进程间都能访问到的区域,来完成数据交换,网络编程就是进程间通信的一种方式,借助的公共区域就是网卡。通过网络编程这种方式既能实现同一主机上的进程间通信,也能实现不同主机上的进程间通信。

2??客户端(client)/服务器(server):

客户端:主动发送数据的一方,获取服务的一方的进程。

服务器:被动接收数据的一方,提供服务一方的进程

因为不知道客户端啥时候发送数据,所以服务器程序一般需要7*24小时运行。

3??请求(request)/响应(response)

客户端给服务器发送的数据称为请求,服务器给客户端发送的数据称为响应。

4??客户端和服务器之间的交互方式

  1. 一问一答:客户端给服务器发送一条请求,服务器返回客户端一个响应(这种方式最常见,比如浏览网页)
  2. 多问一答:客户端给服务器发送多条请求,服务器返回客户端一个响应(比如上传文件,上传文件较大的话可能会有多个请求,上传完了服务器返回一个响应)
  3. 一问多答:客户端给服务器发送一条请求,服务器返回客户端多个响应(下载文件)
  4. 多问多答:客户端给服务器发送多条请求,服务器返回客户端多个响应(游戏)

5??发送端和接收端

发送端:在一次网络数据传输中,数据的发送方进程称为发送端

接收端:在一次网络数据传输中,数据的接收方进程称为接收端

收发端:发送端和接收端两端

一般来说获取一个网络资源有两次网络传输过程:

请求算一次网络传输过程,响应算一次网络传输过程。

网络编程API

在进行网络编程时需要使用操作系统提供的网络编程API,这个API就是系统内核中的传输层提供的API,因为在之前的协议分层中讲过,下一层的协议要给上一层的协议提供服务,上一层的协议要调用下一层的协议,所以下一层的协议要给上一层协议提供API,那我们在网络编程时写的是应用层代码,那就需要传输层给我们提供API,这个API也叫“socket API”。socket本意是插座,翻译为套接字。

一般操作系统的实现,Linux,Windows,Mac都是用C/C++实现的,所以提供的 socket API 是C的,而JVM则把这个API封装了一下,所以用Java代码直接调用这些API就行了。像比如之前的多线程编程API以及现在的网络编程API其实JVM都是封装好了的。

TCP/UDP

简单概括:TCP/UDP

==TCP:==有连接,可靠传输,面向字节流,全双工 (Transmission Control Protocol 传输控制协议)

==UDP:==无连接,不可靠传输,面向数据报,全双工(User Datagram Protocol 用户数据报协议)

有连接:类似于打电话,先连接再通信,无连接:类似于发微信,不用建立连接,直接通信即可

可靠传输:数据对方收没收到,发送方有感知;不可靠传输:数据对方收没收到,发送方无感知

面向字节流:和文件那里的面向字节流是一样的,数据传输以字节为基本单位

面向数据报:数据传输以数据报为基本单位

全双工:双向通信,一个管道,同一时刻,能双向发送数据,类似于双向车道

半双工:单向通信,一个管道,同一时刻,只能单向发送数据,类似于单向车道

双向通信:标准以太网线,里面有八根铜线,四根发送数据,四根接收数据

基于UDP协议编写应用层程序

UDP协议主要API

现在我们要真正用代码编写一个基于UDP协议的服务器/客户端程序。

基于UDP协议编写程序,要使用操作系统提供的UDP网络编程API,这个API中主要有以下几个类

1??DatagramSocket :这个类实际上相当于一个文件,在操作系统中,有一类特殊的文件:socket文件这类文件对应到网卡设备,构造一个DatagramSocket对象就是创建了一个数据报套接字,实际上就是打开了一个内核中的socket文件,之前了解到进程间通信要有一个公共的都可以访问区域,而基于网络实现进程间通信的这个公共区域就是网卡。所以打开这类socket文件就相当于打开了这个公共区域,继而可以接收数据/发送数据

构造方法:

image-20220831214544103

?调用构造方法创建实例就会打开一个socket文件,然后传入的参数是一个端口号,会给当前进程绑定一个端口号。如果调用无参的构造方法,就由操作系统给当前进程分配一个端口号

?如果一个进程需要网络通信,操作系统就会给这个进程分配一个端口号,这个端口号的范围是0~65535,端口号是进程在网络上区分身份的一个标识符,相当于IP地址用于区分网络上的主机。操作系统收接收到网卡的数据报,就可以根据数据报中的端口号来确定把这个数据给到哪个进程

?一个端口号,一般情况下,是不能被多个进程绑定的。而一个进程可以绑定多个端口号

?创建一个DatagramSocket对象就是创建了一个数据报套接字

主要方法

image-20220831214735225

2??DatagramPacket:表示一个UDP数据报,我们知道UDP面向数据报,也就是传输数据单位是数据报,所以这个类就可以构造一个UDP数据报

构造方法:

image-20220831215244212

主要方法

image-20220831215323791

getAddress()是获取主机IP地址,getPortA()是获得端口号

还有一个getSocketAddress() 可以获得IP和端口号,用于构造数据报

回显服务器-客户端程序

我们这里就要使用操作系统提供的UDP协议的API编写一个回显服务器-客户端程序,回显就是客户端发送啥,服务器返回啥,不涉及任何业务逻辑,主要目的是为了测试我们这个程序能否顺利发送接收数据。

服务器程序:

package network;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

public class UdpEchoServer {
    //代表一个socket文件
    private DatagramSocket socket = null;
    //实例化对象实际上就是打开了一个socket文件
    public UdpEchoServer(int port) throws SocketException {
        //给当前程序绑定一个端口号,如果这个端口以及被其他程序绑定,就会抛出异常
        socket = new DatagramSocket(port);
    }
    //启动服务器程序
    public void start() throws IOException {
        System.out.println("服务器已启动!");
        //构造一个空的数据包
        DatagramPacket requsetPacket = new DatagramPacket(new byte[4096],4096);
        while (true){
            //读取客户端发来的请求,如果客户端没有发来请求,就阻塞,等客户端发来请求了,就立即读取数据
            //把读取到的数据放入requsetPacket中,所以这是一个输出型参数
            socket.receive(requsetPacket);
            //对请求进行解析,解析成一个字符串
            String requset = new String(requsetPacket.getData(),0,requsetPacket.getLength());
            //根据请求做出响应
            String response = process(requset);
            //根据响应构造成一个数据报,参数是字符串对应的字节数组,字节数组的长度,以及客户端的Ip和端口号
            DatagramPacket responsePacket = new DatagramPacket
                    (response.getBytes(),response.getBytes().length,requsetPacket.getSocketAddress());
            //把这个数据报返回给客户端(其实是先放到socket文件中了)
            socket.send(responsePacket);
        }
    }

    //由于是一个回显程序,所以接收到啥,就返回啥
    private String process(String requset) {
        return requset;
    }
}

服务器程序:

先打开一个socket文件,这个文件对应到网卡这个公共区域,后续从这个文件里取数据,打开文件的同时,给进程绑定一个端口号。客户端发送请求传输到服务器端的socket文件,服务器从socket文件获取请求并做出响应也会放到这个socket文件,后续客户端上线了,可能继续把响应传输到客户端的socket文件。(黄色背景只为猜测)

基于UDP协议编写应用层程序,数据传输基本单位是数据报,所以从socket文件获取到的数据得用空数据报来接收,后续根据请求做出的响应也得打包成数据报,再发送给客户端

1??先构造一个空的数据报用来接收请求,然后使用receive()把接收到的请求放到空数据报中,再对这个数据报做出解析(接收数据报并解析

2??根据解析结果做出响应:把接收到的请求解析成字符串,根据这个字符串做出响应,响应也是字符串(根据解析做出响应

3??把响应和客户端IP,客户端程序的端口号一起打包成一个数据报,发送给客户端(打包响应为数据报并返回客户端

注意:根据响应字符串构造响应数据报时,传入的参数是响应字符串对应的字节数组,字节数组的长度,客户端程序的Ip和端口号。 第二个参数是字节数组的长度不是字符串的长度,因为字符串里如果有中文字符的话,一个中文字符占用的空间就不是一个字节了,可能是三个字节。比如 “a哈” 这个字符串的长度是2,但是转换为字节数组的长度是4。

客户端程序:

package network;

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket = null;
    public UdpEchoClient() throws SocketException {
        socket = new DatagramSocket();
    }
    public void start() throws IOException {
        while (true){
            System.out.println(">");
            Scanner scanner = new Scanner(System.in);
            //接收用户请求
            String requset = scanner.next();
            //将用户请求打包成数据报,并发送给服务器,指定服务器IP,和服务器程序的端口号
            DatagramPacket requsetPacket = new DatagramPacket
                    (requset.getBytes(),0,requset.getBytes().length, InetAddress.getByName("127.0.0.1"),8888);
            socket.send(requsetPacket);
            //构造空的数据报接收服务器发来的响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(responsePacket);
            //解析响应
            String response = new String(responsePacket.getData(),0,responsePacket.getLength());
            System.out.printf("请求:%s ,响应:%s",requset,response);
            System.out.println();
        }
    }

    public static void main(String[] args) throws IOException {
        UdpEchoClient client = new UdpEchoClient();
        client.start();
    }
}

1??客户端先接收用户输入的信息,然后组合服务器的IP,和服务器程序端口号打包成一个数据报,把这个数据报发送给服务器。(打包请求并发送

2??然后,构造一个空的数据报,用来接收服务器返回来的响应,使用receive()把接收到的数据放到数据报中,如果服务器还没有返回响应,当执行到receive()这里就会阻塞等待,等响应返回过来之后,receive()就返回(接收响应

3??把收到的响应解析出来,呈现给用户。

在构造请求数据报时的参数:InetAddress.getByName()表示数据报发给谁,然后这个方法的参数填入127.0.0.1,这是一个环回Ip,表示当前主机。


一般服务器要服务多个客户端,所以我们可以使用这个一客户端程序打开多个客户端,看看服务器能否服务多个客户端,具体步骤:

image-20220901144958847

image-20220901145209249

经过上面的步骤就可以打开多个客户端,测试我们的服务器是否能为多个客户端提供服务了

?服务器的端口一般是手动指定的,因为如果服务器这边的端口是系统分配的,客户端就不知道服务器的端口是啥了,那就没法传输数据了

?而客户端的端口号一般是系统分配的,因为客户端是安装在用户电脑上的,用户很多,用户上的程序占用了哪些端口也不确定,如果给客户端指定一个端口号,这个端口号可能被其他程序占用了,系统就没法给客户端分配这个端口号了。

基于TCP协议编写应用层程序

TCP协议主要API

1??ServerSocket:这是服务端使用的Socket

构造方法:

image-20220903100709122

构造SercerSocket对象,并指定一个端口号

主要方法:

image-20220903100949266

?accept()的工作是拉客,建立TCP连接是内核的工作(当有客户端来申请建立连接时,内核就建立连接),而accept()的工作是把建立好的连接拿到应用程序中。如果相应连接没有建立,accept()就阻塞。直到建立好连接。建立连接之后,accept()就会返回,返回值是一个Socket对象,通过这个对象和客户端进行交互

2??Socket:服务器和客户端都会使用的API

客户端使用Socket:用于和服务端建立连接,保存对端的数据,及时与对方收发数据

服务端的Socket是建立连接后返回的Socket,用于保存对端数据,及时与对方收发数据

构造方法:

image-20220903102129931

主要方法:

image-20220903102158727

Socket里面包含了输入 输出流对象,也就是通过输入输出流来发送接收数据

回显服务器-客户端程序

服务端程序:

package network;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TcpEchoServer {
    ServerSocket serverSocket = null;
    public TcpEchoServer(int port) throws IOException {
        //绑定端口号
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动");
        //创建线程数目动态增长的线程池
        ExecutorService threadPool = Executors.newCachedThreadPool();
        //服务端应该一直获取连接,处理连接(不能处理了一次连接服务器就结束了,所以获取连接和处理连接应该放入循环)
        while (true){
            //获取连接(如果有客户端申请建立连接,就返回,否则阻塞等待)
            Socket clientSocket = serverSocket.accept();
            //处理当前连接(在当前连接处理过程中,服务器是没法和其他客户端建立连接的,
            // 因为处理连接执行完,才能执行accept())
            // 1.处理连接在主线程执行,效率低
            //processConnection(clientSocket);

            // 2.创建新线程来处理连接,效率高;主线程只是获取连接,创建新线程,处理连接这个工作交给新线程来完成
//            Thread thread = new Thread(() ->{
//                try {
//                    processConnection(clientSocket);
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            });
//            thread.start();

            //3.当客户端较多时,需要频繁创建线程销毁线程,这也是有资源开销的,所以可以利用线程池,减少线程创建销毁
            threadPool.submit(() ->{
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        }
    }
    //处理连接,此处写法为长连接
    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("建立连接:客户端IP:%s 端口号:%s\n",
                clientSocket.getInetAddress().toString(),clientSocket.getPort());
        //获取到的Socket内置了输入输出流,因为TCP协议面向字节流,
        // 而InputStream,OutputStream正好是面向字节流,所以内置的是这两个流
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()){
            //对输入输出流做一层包装,这样在获取请求时和返回响应时更方便
            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            //当Socket对象有了,两个流有了,就相当于数据源和数据目的地也有了,数据转移的公路也有了,
            //接下来就可以获取请求,返回响应了,
            while (true){
                //如果Socket中没数据,会在hasNext()这里阻塞,等客户端传来数据,就返回true,
                //读到EOF返回false,只有当客户端程序结束,才能读到EOF,返回false
                if (!scanner.hasNext()){
                    System.out.printf("断开连接:客户端IP:%s,端口号:%s\n",
                            clientSocket.getInetAddress().toString(),clientSocket.getPort());
                    break;
                }
                //从clientSocket读取字符串,读到空白字符结束
                String request = scanner.next();
                //根据请求计算响应
                String response = process(request);
                //把数据写入Socket,并且附带一个'\n'(换行符)
                printWriter.println(response);
                //因为是先把数据放入输出缓冲区,所以不一定就把缓冲区中的数据写入Socket了,
                // 应该刷新一下缓冲区,缓冲区中的数据才能真正给到Socket
                printWriter.flush();
                System.out.printf("返回响应:[%s:%s],req:%s,resp:%s\n"
                ,clientSocket.getInetAddress().toString(),clientSocket.getPort()
                ,request,response);
            }
        }finally {
            clientSocket.close();
        }
    }

    //针对请求处理响应
    private String process(String requset) {
        return requset;
    }

    public static void main(String[] args) throws IOException {
        TcpEchoServer tcpEchoServer = new TcpEchoServer(8888);
        tcpEchoServer.start();
    }
}

?TCP服务器执行流程:获取连接并针对连接处理,因为服务器要能够连接很多客户端,所以获取连接并处理连接这个过程应该在循环里,while(true) {获取连接 ,处理连接}

?虽然在不停的获取连接,处理连接,但是当一个客户端连接上了,在处理连接的过程中,别的客户端是没法建立连接的(执行不到accept()),只有当第一个客户端断开连接后(执行到accept()),别的客户端才有可能建立上连接,这就很不合理,客户端比较多的话,这也就太慢了,所以解决办法是:把处理连接这个过程放在一个新线程中执行,让主线程只是不停的尝试获取连接,获取到一个连接,就创建一个新线程来处理连接。

?当服务端获取到一个连接,处理连接时,应先打开对应到Socket的两个流对象,然后拿到请求,处理请求,返回响应,这里还涉及到长短连接。

==长连接:==一次连接,然后不停的收发数据,可以有多次请求+多次响应(长连接应该使用循环接收请求,返回响应

==短连接:==建立一次连接,只有一次收发数据(一次请求+一次响应),然后就断开连接

==到底使用短连接还是长连接,得看场景:==对于客户端请求不频繁的场景,适合使用短连接(比如浏览网页);对于客户端请求频繁的场景,适合使用长连接(比如实时游戏)

?实际上当客户端和服务器建立连接之后(建立连接是内核的工作),就会产生一个Socket文件。然后,客户端就可以往Socket文件发送请求,从Socket文件接收响应,然后等服务器这边获取到连接之后,服务器程序就可以从Socket文件获取请求,往Socket文件发送响应。当服务器获取到连接之后就可以处理连接:先==scanner.hasNext()==判断Socket文件中有没有数据(即还有没有存于Socket文件中的请求未处理),如果没有数据就阻塞,等客户端发送请求,如果有数据就返回True,当客户端程序结束的时候,那对应的Socket文件就没了,那scanner.hasNext()就会返回False,然后退出循环。(也就是除非客户端那边程序结束,否则连接就不会断开,所以这是长连接,一次连接,可以处理多次请求)

客户端程序:

package network;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

public class TcpEchoClient {
    private Socket socket = null;
    public TcpEchoClient() throws IOException {
        //和服务器建立连接
        socket = new Socket("127.0.0.1",8888);
    }
    public void start() throws IOException {
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()){
            Scanner scannerNet = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            Scanner scanner = new Scanner(System.in);
            while (true){
                System.out.print(">");
                //读取用户请求
                String request = scanner.next();
                //把请求发送给服务端
                printWriter.println(request);
                //刷新一下缓冲区
                printWriter.flush();
                //读取响应
                String response = scannerNet.next();
                System.out.printf("请求:%s,响应:%s\n",request,response);
            }
        }
    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient tcpEchoClient = new TcpEchoClient();
        tcpEchoClient.start();
    }
}

?客户端当根据服务器的IP地址,和服务器的进程的端口号创建一个Socket对象时,已经建立好和服务器的连接了。后续就可以用这个Socket对象和服务器进行交互。

?客户端的逻辑得根据实际场景,我们这里自己写的逻辑是不停的读取用户输入,然后把用户输入发送给服务器,然后获取响应。注意:==这里把用户请求发送给Socket时,是先把数据放到输出缓冲区了,所以缓冲区的内容不一定发送给Socket对象了,也就是服务器可能并没有接收到请求,==所以当客户端发送请求后应该刷新一下缓冲区,才能避免请求待在缓冲区中而没有发送给服务器。服务器那边也是同理,当发送响应后,应该刷新一下缓冲区。

?我们的服务器和客户端都是使用scanner.next()获取请求或响应,注意:这种方法获取数据是遇到空白字符才能返回,把空白字符之前的数据返回,所以得保证一个请求的末尾是一个空白字符,如果客户端发送了一个请求,但是末尾没有空白字符,那服务器就会阻塞到scanner.next()这里了,如果我们的请求当中有一个空白字符,那就只获取到半个请求,针对半个请求计算响应?(这显然是不合理的),你得保证每个请求之间有空白字符分隔,还得保证请求中间没有空白字符,才能正确读到每次请求,所以我们的这种写法局限性很大。

问题:

  1. socket.close()作用
  2. 为啥客户端程序结束后,scanner.hasNext()读到EOF并返回false
  3. 粘包问题
  4. 怎么样算断开连接
  5. 如果使用read()读取请求,该怎么区分开每次的请求呢?自定义应用层协议,固定一个数据报的长度
  6. IO多路复用是干啥的(NIO)
  7. jconsole查看调用栈
  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-09-13 11:56:34  更:2022-09-13 11:58:53 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年6日历 -2024/6/29 17:49:44-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码