网络编程概念

什么是网络编程?

网络编程:就是我们书写代码实现:对于不同的进程之间,进行网络通信(网络数据的传递)

注意:进行通信的主机是同一台主机或者不同主机都可行(只要是两个不同的进程就行)

我们日常通过访问网页获取需要的网络资源(视频,图片)就是使用网络编程实现的。


如何实现网络编程?

网络编程的实质就是:传递数据+编写代码

因此,我们只要完成这两部分就能实现网络编程。

传递数据:

我们网络通信传递数据是有“协议”的--规定了数据传输的规则,这些“协议”来自于“传输层协议”(普遍使用的是UDP或者TCP协议)。那么我们就要简单先了解一下这两个传输层协议的特性,理解我们传输数据规则的大概要求,后续再详细介绍这些协议。

1.UDP协议:无连接,不可靠传输,面向数据报,半双工

  • 无连接:每次数据的传输,直接传输,不用保存通信双方信息(我只要知道发给谁,直接发就行)
  • 不可靠传输:数据在传输过程中可能会出现“丢包”(数据丢失),UDP不会管这些丢失的数据,通信双方也不知道是否有数据丢失(但是传输效率更高)
  • 面向数据报:数据会被构造成一个“数据报”形式才会在网络中传输
  • 半双工:一个通信只能“单向传输”

2.TCP协议:有连接,可靠传输,面向字节流,全双工

  • 有连接:通信双方要在传输数据之前保留双方核心信息(IP和端口号),断开连接后删除信息
  • 可靠传输:数据在传输过程中可能会出现“丢包”(数据丢失),TCP协议会尝试重传数据(对抗丢包),就算最后仍然“丢包”,双方都会知道是否有数据丢失(传输效率相较于较低)
  • 面向字节流:数据按照字节流传输(类似文件IO操作),更加灵活
  • 全双工:一个通信能“双向传输”

编写代码:

编写代码其实就是,我们程序员在应用层编写代码调用传输层(操作系统)接口(API),从而配置好“协议”以及进行通信传输


网络编程的基本概念

我们知道网络编程是双方的网络通信,因此我们通过一个常见的网络编程例子,辅以解释我们网络编程中常见到的概念:

  • 客户端:请求服务的一方(比如,请求下载视频的我们主机)
  • 服务端:给请求提供服务的一方(提供下载视频的资源网站)
  • 请求:一般是先发送信息的一方进行的操作
  • 响应:收到消息后做做出回应的一方的操作
  • 发送端:源主机
  • 接收端:目的主机

网络编程套接字Socket

上面我们说到进行网络编程的进行需要“编写代码”,我们编写代码本质就是在应用层使用代码调用操作系统的接口,使用其中实现的传输层“协议”。

这个操作系统提供的接口就是Socket,通过Socket我们可以调用传输层中协议规定数据形式的实现。(可以理解成Socket就像“遥控器”,遥控我们的“空调”(网卡)进行传输数据的规范和传输)

我们常使用的Socket有两类:

流套接字:使用传输层TCP协议(具有TCP协议的特性)

数据报套接字:使用传输层UDP协议(具有UDP协议的特性)

马上我们会介绍到两类套接字实现的通信流程的工作流程。

UDP数据报套接字编程

API介绍

DatagramSocket

DatagramSocket是UDP Socket用于发送和接收UDP数据报

构造方法:

方法签名 方法说明
DatagramSocket() 创建一个UDP数据报套接字的Socket,绑定到任意随机端口(一般用于客户端)
DatagramSocket(int port) 创建一个UDP数据报套接字的socket,绑定到指定端口(一般用于服务端)

注:服务端指定端口号:方便客户端找到它,发送请求给它

客户端随机端口号:一般客户端在我们主机上,端口号是由操作系统分配的(不定),并且服务端可以根据我们发送过去的数据报提取出我们客户端的“端口号”

功能方法:

方法签名 方法说明
void receive(DatagramPacket p) 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)
void send(DatagramPacket p) 从此套接字发送数据报包(不会阻塞等待,直接发送)
void close() 关闭此数据报套接字

注:receive方法是“输出型函数”,相当于传进去一个参数作为“容器”承载数据回来。

DatagramPacket

DatagramPacket是UDP socket发送和接收的数据报(数据想要发送就要先封装成该形式)

构造方法(常用的):

方法签名 方法说明
DatagramPacket(byte[] buf,int length) 构造一个DatagramPacket以用来接收数据报,接收的
数据保存在字节数组(第一个参数buf)中,接收指定
长度(第二个参数length)
DatagramPacket(byte[] buf,int offset,int length,SocketAddress address) 构造一个DatagramPacket以用来发送数据报,发送的
数据为字节数组(第一个参数buf)中,从0到指定长
度(第二个参数length)。address指定目的主机的IP
和端口号​

功能方法:

方法签名 方法说明
InetAddress getAddress()​ 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址​
int getPort() 从接收的数据报中,获取发送端主机的端口号;或从
发送的数据报中,获取接收端主机端口号
byte[] getData() 获取数据报中的数据

UDP通信模型实现

图示就是UDP通信模型工作的流程和需要实现的操作。

代码示例:

Sever(服务器):

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

public class UDPSever {
    //基于UDP协议进行网络通信的服务器
    //操纵网卡的遥控器
    DatagramSocket socket=null;
    //构造方法
    //服务器的端口号要指定,这样客户端才能找到(系统随机则找不到)
    public UDPSever(int port) throws SocketException {
        socket=new DatagramSocket(port);
    }

    //服务器启动
    public void start() throws IOException {
        System.out.println("服务器已启动");
        //服务器的基本工作流程
        while (true) {
            //1.接收客户端请求
            //构造数据报接收
            DatagramPacket datagramPacket = new DatagramPacket(new byte[1024], 1024);
            socket.receive(datagramPacket);
            //假设我们传递的都是字符串,将数据报解析成原始要求
            String request = new String(datagramPacket.getData(),0, datagramPacket.getLength());
            //2.处理客户端请求
            String response = process(request);
            //根据响应内容,构造数据报发送回去
            //值得注意的是,我们UDP无连接特性,因此客户端的IP和端口从请求数据报中提取
            DatagramPacket resPacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                    datagramPacket.getAddress(), datagramPacket.getPort());
            //3.返回响应给客户端
            socket.send(resPacket);
            //4.打印日志报告
            System.out.printf("[%s:%d]客户端:req:%s,res:%s\n", datagramPacket.getAddress(), datagramPacket.getPort()
                    , request, response);
        }
    }
//这里写一个最简单的回显服务器,请求是什么,就重复一遍请求当作响应发送回去
    private String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        UDPSever udpSever=new UDPSever(9090);
        udpSever.start();
    }
}

Client(客户端):

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

public class UDPClient {
    //基于UDP协议进行网络通信的客户端
    DatagramSocket datagramSocket=null;
    String SeverIP;
    int SeverPort;
    //构造方法
    //客户端端口根据操作系统进行分配(因为无所谓)
    public UDPClient(String ip,int port) throws SocketException {
        //因为UDP的无连接特性,每次启动服务器都要用IP和端口号连接一次服务器(也不算连接,就是知道发送对象是谁)
        this.SeverIP=ip;
        this.SeverPort=port;
        datagramSocket=new DatagramSocket();
    }

    //启动客户端
    public void start() throws IOException {
        System.out.println("客户端已启动");
        //客户端的工作基本流程
        //1.发送请求给服务器
        while (true) {
            System.out.println("请输入你要发送的请求->");
            Scanner scanner = new Scanner(System.in);
            String request = scanner.next();
            if (request.equals("exists")) {
                return;
            }
            //根据请求构造发送的数据报
            DatagramPacket reqPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(SeverIP), SeverPort);
            datagramSocket.send(reqPacket);
            //2.接收服务器返回响应
            //接收响应数据报
            DatagramPacket resPacket = new DatagramPacket(new byte[1024], 1024);
            datagramSocket.receive(resPacket);
            //3.解析响应
            String response =new String(resPacket.getData(),0,resPacket.getLength());
            //4.展示服务器响应结果
            System.out.println("服务器响应结果是:" + response);
        }
    }

    public static void main(String[] args) throws IOException {
        UDPClient udpClient=new UDPClient("127.0.0.1",9090);
        udpClient.start();
    }
}

注:1.传输的数据是封装成数据报的形式,发送还是解析都要先构造数据报或者分用数据报

2.由于UDP的无连接特性,因此服务器传回IP与端口号,需要从接收的请求数据报中去提取


TCP流套接字编程

API介绍

ServerSocket

serversocket是创建TCP服务端Socket的API

构造方法:

方法签名 方法说明
ServerSocket(int port) 创建一个服务端流套接字Socket,并绑定到指定端口​

功能方法:

方法签名 方法说明
Socket accept() 开始监听指定端口(创建时绑定的端口),有客户端
连接后,返回一个服务端Socket对象,并基于该
Socket建立与客户端的连接,否则阻塞等待
void close() 关闭此套接字

这个类的主要作用就是领着“客户端的请求”来找到创建Socket文件对象,进行真正的客户端,服务器连接,进行后续的请求,响应操作

Socket

Socket是客户端的Socket,或服务器中接收到客户端建立连接(accept方法)请求后,返回的服务器Socket。

不管是客户端的,还是服务器的Socket,都是双方建立连接以后,保存对端信息,及用来与对方收发数据的

构造方法:

方法签名

方法说明
Socket(String host,int port) 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接

功能方法:

方法签名 方法说明
InetAddress getInetAddress() 返回套接字所连接的地址
InputStream getInputStream() 返回此套接字的输入流
OutputStream getOutputStream() 返回此套接字的输出流

TCP通信模型实现

图示为TCP流套接字的网络通信基本流程

Sever(服务器):

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 TCPSever {
    //作为接待客户端连接请求
    ServerSocket serverSocket = null;

    //构造方法,指定端口号便于被客户端指定连接
    public TCPSever(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    //启动服务器
    public void start() {
        System.out.println("服务器启动");

        //
        ExecutorService executorService = Executors.newCachedThreadPool();
        //服务器要一直运行处理客户端请求
        while (true) {
            executorService.submit(() -> {
                Socket socket = null;
                try {
                    socket = serverSocket.accept();
                    processConnection(socket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });

        }
    }

    //真正处理连接
    private void processConnection(Socket socket) {
        System.out.printf("[%s:%d]服务器已经上线\n", socket.getInetAddress(), socket.getPort());

        //开始进行真正通信
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream();
             PrintWriter printWriter = new PrintWriter(outputStream)) {
            Scanner scanner=new Scanner(inputStream);
            //服务器的基本工作流程
            while (true) {
                //1.接收客户端请求
                if(!scanner.hasNext()){
                    System.out.printf("[%s:%d]客户端已经下线\n",socket.getInetAddress(),socket.getPort());
                    break;
                }
                String request=scanner.next();
                //2.处理客户端请求
                String response=process(request);
                //3.返回服务器的响应
                printWriter.println(response);
                printWriter.flush();
                //4.打印日志
                System.out.printf("[%s:%d]客户端:req:%s,res:%s\n",socket.getInetAddress(),socket.getPort(),request,response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private String process(String s) {
        return s;
    }

    public static void main(String[] args) throws IOException {
        TCPSever tcpSever=new TCPSever(9090);
        tcpSever.start();
    }
}

Client(客户端):

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 TCPClient {
Socket socket=null;
//存储服务器的IP和端口号
    String SeverIP;
    int SeverPort;

    //构造方法
    public TCPClient(String IP,int port) throws IOException {
        this.SeverIP = IP;
        this.SeverPort = port;
        socket = new Socket(SeverIP, SeverPort);
    }

    //启动客户端
    public void start() {
        System.out.println("客户端启动");

        //客户端的工作基本流程
        try(InputStream inputStream=socket.getInputStream();
            OutputStream outputStream=socket.getOutputStream();
            PrintWriter printWriter=new PrintWriter(outputStream)) {
            Scanner req=new Scanner(System.in);
            while (true) {
                //1.用户输入请求
                System.out.println("请输入你的请求->");
                String requeset=req.next();
                if(requeset.equals("exists")){
                    break;
                }
                //2.发送用户请求给服务器
                printWriter.println(requeset);
                printWriter.flush();
                //3.接收服务器的响应信息
                Scanner scanner=new Scanner(inputStream);
                if(!scanner.hasNext()){
                    break;
                }
                String response=scanner.next();
                //4.展示响应信息结果
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws IOException {
        TCPClient tcpClient=new TCPClient("127.0.0.1",9090);
        tcpClient.start();
    }
}

注:1.由于服务器要应对多个客户端的连接,因此这里采用“线程池”来处理“accept的连接客户端和服务器”操作

2.服务器中的ServiceSocket相当于“接客前台”的作用,领着客户端到真正“负责连接”的人

Socket相当于是“负责连接”的人,建立连接后,由Socket处理后续的请求和响应操作(相当于前台接过来,然后socket进行服务)


长短连接

我们知道TCP的特性(有连接),因此,客户端和服务器传输数据连接时间的长短就决定了,此次连接是长连接,还是短连接。

  • 长连接:进行多次传输请求和接收响应,双方一直保持着连接状态(可以进行多次通信)
  • 短连接:进行一次传输请求和接收响应之后,就断开连接(只进行一次通信)

关于长,短连接也有各自的优劣之处以及使用场合:

  • 短连接适合于客户端使用(一般客户端拿到自己需要的数据后,就会断开了),客户端与服务器的连接次数少的情况(请求频次少,比如查阅网页资料)
  • 长连接适合于需要一直进行连接传输的场景(网络聊天和打游戏)
  • 短连接连接,中断频次高,占用资源,效率较低;长连接只进行一次连接断开,比较节省资源,效率较高

同时,关于提升我们TCP服务器的效率还有“IO多路复用”--一个线程处理多个socket(同一时刻,连接的客户端多,但不是每个客户端都在传输数据,因此,一个线程处理多个可以尽可能节省资源,提升效率),但是IO多路复用在Java标准库中封装的过于麻烦,大家有兴趣可以去了解。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐