JavaEE初阶:网络编程

目录

[1. 为什么需要网络编程](#1. 为什么需要网络编程)

[2. 什么是网络编程](#2. 什么是网络编程)

[3. 网络编程的具体应用场景](#3. 网络编程的具体应用场景)

[3.1 常见的客户端服务端模型](#3.1 常见的客户端服务端模型)

[4. Socket套接字](#4. Socket套接字)

[4.1 概念](#4.1 概念)

[4.2 TCP与UDP的区别](#4.2 TCP与UDP的区别)

[4.2.1 有连接 vs 无连接](#4.2.1 有连接 vs 无连接)

[4.2.2 可靠传输 vs 不可靠传输](#4.2.2 可靠传输 vs 不可靠传输)

[4.2.3 面向字节流 vs 面向数据报](#4.2.3 面向字节流 vs 面向数据报)

[4.2.4 全双工 vs 半双工](#4.2.4 全双工 vs 半双工)

[4.3 UDP数据报套接字编程](#4.3 UDP数据报套接字编程)

[4.3.1 DatagramSocket](#4.3.1 DatagramSocket)

[4.3.2 DatagramPacket](#4.3.2 DatagramPacket)

[4.3.3 数据报套接字网络编程](#4.3.3 数据报套接字网络编程)

[4.3.3.1 UdpEchoServer](#4.3.3.1 UdpEchoServer)

[4.3.3.2 UdpEchoClient](#4.3.3.2 UdpEchoClient)

[4.3.3.3 客户端与服务器互信效果视频](#4.3.3.3 客户端与服务器互信效果视频)

[4.4 TCP流套接字编程](#4.4 TCP流套接字编程)

[4.4.1 ServerSocket](#4.4.1 ServerSocket)

[4.4.2 Socket](#4.4.2 Socket)

[4.4.3 流套接字网络编程](#4.4.3 流套接字网络编程)

[4.4.3.1 TcpEchoServer](#4.4.3.1 TcpEchoServer)

[4.4.3.2 TcpEchoClient](#4.4.3.2 TcpEchoClient)

[4.4.3.3 流套接字编程优化版](#4.4.3.3 流套接字编程优化版)

[4.4.3.4 最终版流套接字编程](#4.4.3.4 最终版流套接字编程)


通过上篇"初始网络原理",我们知道了主机间的网络数据传输,要经过从应用层------》物理层 的TCP/IP五层协议模型,这种概念。而网络数据传输是通过网络编程实现的,接下来本篇将梳理网络编程相关概念,并通过网络编程实现网络传输功能(也就是实现服务端------客户端 互信)

1. 为什么需要网络编程

网络编程是为了获取丰富的网络资源 ;打开浏览器一系列的数据信息涌入主机,打开腾讯视频,丰富的视频资源、图片资源、文本资源等,为你而来。

2. 什么是网络编程

网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信。****(小白们不用想了,你们是理解不了的,在看完这堆破概念以后,从Socket套接字开始深入思考,结合代码才能理解)

  • 网络程序是指为实现网络通信而设计的、能在不同设备间传输数据的软件程序

基于Socket API开发的网络程序都是网络编程。但就算是同一台主机, 只要是不同进程(程序),不管是否连网,只要基于Socket进行数据传输也属于网络编程。

注意:不管是网络上的设备,还是同一台主机进行数据传输也好,发请求的和响应的进程都是不是同一个。

虽然在同一台主机上也可以实现网络编程,但要明确网络编程的目的是基于网络,给网络上的不同主机传输数据资源。

3. 网络编程的具体应用场景

网络编程广泛应用于服务端和客户端交互场景:

注意:谁发送请求谁就是客户端,谁处理请求返回响应谁就是服务端,响应不一定是发给发送请求的主机,响应也可以返回给其他主机。

3.1 常见的客户端服务端模型

  1. 客户端发送请求
  2. 服务端接收请求
  3. 服务端根据请求进行一系列计算处理,生成响应
  4. 返回响应
  5. 客户端解析响应,展示处理结果

上面的客户端和服务端模型,就像去饭店吃饭一样:你走进饭店说,老板来份辣椒炒肉(请求),老板说好嘞(接收请求),厨房就开始烹饪(处理业务),然后菜就端过来(返回响应),展现到你面前了。


思考: 网络编程是网络通信的基础。上图中,客户端与服务端互信时的请求、响应和处理业务的逻辑,都是程序员在++应用层(TCP/IP五层协议模型中的应用层)++ 自定义实现的,程序员想怎么定义,就怎么定义。咱Java程序员大多时候也都是在和应用层打交道,只调用传输层的api进行网络编程。而操作系统也提供了用于网络编程的api------》Socket


4. Socket套接字

4.1 概念

注意:计算机中的文件,通常指的是广义上的文件------》硬件也被抽象成文件方便管理。网卡抽象出来的文件,就是Socket文件,读写Socket,其实就是在读写网卡。

Socket 的中文专业术语叫**"套接字",这个专业术语就很奇怪;Socket的中文是插座,**但是不知道怎么回事就叫套接字了。

Socket封装了传输层的协议,封装的协议中就最主要的网络协议就是TCP和UDP,我们进行编程时只需要调用Socket,把数据传输给传输层,后面传输层到物理层的各种传输就不用管了,因为后面各层也已经封装好了(不需要关注后面,只调用使用即可)。

  1. Socket是操作系统提供给应用层调用的API,旨在方便进行网络编程。
  2. Socket是基于TCP/IP协议模型的网络通信的基本操作单元。
  3. 基于Socket的网络程序开发就是网络编程。

TCP、UDP风格差别很大,因此Socket也对应这提供了两套------》流套接字数据报套接字

(Socket有多种,但主要的还是上述两种套接字)


流套接字基于TCP进行封装,数据报套接字基于UDP进行封装。

要想知道这俩套接字的区别,知道TCP与UDP协议的区别就行了。


4.2 TCP与UDP的区别

传输层两大核心协议:TCP、UDP,它们的风格差别很大。

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

UDP:无连接,不可靠传输,面向数据报,全双工

4.2.1 有连接 vs 无连接

协议有无连接,指的是在网络通信中,TCP保存了对端信息------》叫有连接;UDP协议本身不会保存对端的信息------》叫无连接。保存的主要信息就是那"五元组"。

连接指的是逻辑上的连接,我拥有你的IP、端口号信息才能传输数据给你。

奇思妙想:有人想问UDP协议不保存对端信息,怎么进行网络编程然后网络互信的?其实UDP本身不保存对端信息,但我们编程时,可以用变量手动记录对端信息。

4.2.2 可靠传输 vs 不可靠传输

可靠传输 :当传输的数据发生丢失(也就是丢包),TCP通过再次发送数据包提高传输成功率(可靠传输具体怎么实现的此处不讲,但一点也不影响后续编程),但成功率可不是100%,可靠传输不保证数据包能100%到达对端。

不可靠传输:发出数据包就不管了。

4.2.3 面向字节流 vs 面向数据报

面向字节流:读写数据,以字节为单位。

面向数据报:读写数据,以一个数据报为单位(不是字符,一个数据报的空间比字符大很多,读写能传输半个数据报)

4.2.4 全双工 vs 半双工

全双工:数据传输是双向的,两端可互相通信。

半双工:数据传输为单向,A能传给B,但B不能传给A。

4.3 UDP数据报套接字编程

Java基于原生UDP数据报套接字进行了封装,给出了两个类:

  • DatagramSocket:是UDP Socket ,用于发送和接收数据报。
  • DatagramPacket:是UDP Socket发送和接收的数据报

以下是网络编程接口的常用方法:

4.3.1 DatagramSocket

构造方法:

|--------------------------|-----------------------------------------------|
| 方法签名 | 方法说明 |
| DatagramSocket() | 创建一个UDP数据报套接字(Socket),绑定到本机一个随机端口上。(一般用在客户端) |
| DatagramSocket(int port) | 创建一个UDP数据报套接字(Socket),绑定到本机一个指定的端口上。(一般用在服务端) |

方法:

|---------------------------------|----------------------------|
| 方法签名 | 方法说明 |
| void receive(DatagramPacket dp) | 从此套接字接收数据报(没接收到数据报,方法就会阻塞) |
| void send(DatagramPacket dp) | 通过该方法发送数据报(不会阻塞,有就直接发送) |
| void close() | 关闭数据报套接字 |

new DatagramSocket对象,进程描述符表就创建了一个项,代表打开了一个文件(此处打开了Socket文件)。在Socket概念中讲过,Socket文件是网卡的代理,文件操作步骤也就3步,1.打开文件,2.读写文件,3.关闭文件

4.3.2 DatagramPacket

构造方法:

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

方法:

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

4.3.3 数据报套接字网络编程

4.3.3.1 UdpEchoServer

编程回显服务器(省略处理请求逻辑,请求发来什么,就响应什么,这就是回显):

或许有人注意到,代码中没有使用close()关闭套接字,疑问:这样不会导致文件描述符表耗尽出bug吗?

答:上面套接字不需要关闭(准确的说是不需要手动关闭);圆圈步骤2new 的UDP Socket(打开了Socket文件)被步骤1的成员变量引用着,while循环一直在接收处理不同客户端的请求,所以我们不需要Socket.close()手动关闭Socket文件,等到程序关闭,Socket文件会自动关闭。

java 复制代码
package udpSocket;

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

public class UdpEchoServer {
    private DatagramSocket socket = null;

    public UdpEchoServer(int port) throws SocketException {
        //指定一个端口号给服务器绑定使用
        socket=new DatagramSocket(port);
    }
    //===================
    //网络编程总结为3步:
    //1.接收请求
    //2.处理请求
    //3.返回响应
    //===================
    //开启服务器
    public void start() throws IOException {
        //服务器一般是7*24小时开着的,遇到请求就处理,所以使用while死循环
        while(true){
            //构造接收数据报,字节数组为载荷部分,且数组为空(全为0)
            DatagramPacket request = new DatagramPacket(new byte[4096],4096);
            //使用上面的数据报接收请求,(receive()的参数为输出型参数)
            //1.接收请求
            socket.receive(request);
            //为了好处理,转化成字符串(只将有效数据转换,也就是request.getLength())
            String requestStr = new String(request.getData(),0, request.getLength());

            //2.处理请求,生成响应
            String responseStr = process(requestStr);
            //构造响应数据报
            DatagramPacket response = new DatagramPacket(responseStr.getBytes(),0,responseStr.getBytes().length,request.getSocketAddress());
            //3.返回响应
            socket.send(response);

            //打印日志
            System.out.printf("[%s:%d] req:%s resp:%s\n",request.getAddress().toString(),request.getPort(),requestStr,responseStr);
        }
    }

    private String process(String requestStr) {
        //因为是回显服务器,因此不做任何处理,直接请求数据
        return requestStr;
    }

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

编程客户端

有了编写服务端(服务器)的经验,编写客户端就简单的多了:

java 复制代码
package udpSocket;

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

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String address;
    private int port;

    public UdpEchoClient(String address,int port) throws SocketException {
        //构造参数不指定固定端口,系统会随机分配一个空闲端口。客户端一般不指定端口
        socket = new DatagramSocket();
        //记录目的IP和目的端口号
        this.address=address;
        this.port=port;
    }
    //===================
    //1.用户输入内容
    //2.发送请求
    //3.接收响应
    //4.解析响应数据
    //===================
    //开启客户端
    public void start() throws IOException {
        Scanner sc = new Scanner(System.in);
        //客户端也得随时应对用户
        while(true){
            //1.用户输入数据
            System.out.println("请输入内容:");
            if (!sc.hasNext()){
                return ;
            }
            String requestStr = sc.next();
            //构造发送数据报
            DatagramPacket request = new DatagramPacket(requestStr.getBytes(),0,requestStr.getBytes().length,InetAddress.getByName(address),port);

            //2.发送数据
            socket.send(request);

            //3.接收响应
            DatagramPacket response = new DatagramPacket(new byte[4096],4096);
            socket.receive(response);

            //4.解析响应
            String processStr = new String(response.getData(),0,response.getLength());
            String responseStr = process(processStr);
            System.out.println(responseStr);
        }
    }

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

    public static void main(String[] args) throws IOException {
        UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1",8080);
        udpEchoClient.start();
    }
}
4.3.3.3 客户端与服务器互信效果视频

UDP数据报套接字实现网络编程效果视频

额。视频上传后要收费,这里自己演示,不复杂的。。。

4.4 TCP流套接字编程

Java封装了TCP套接字,给出了两个类:

  • ServerSocket 专门提供给服务端使用
  • Socket 提供给服务端和客户端使用

TCP套接字没有receive()和send(),而是通过"输入输出流"读写Socket文件内容(读写网卡文件)

4.4.1 ServerSocket

构造方法:

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

方法:

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

4.4.2 Socket

Socket类可不是操作系统提供的那个Socket套接字。

构造方法:

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

方法:

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

4.4.3 流套接字网络编程

4.4.3.1 TcpEchoServer
java 复制代码
package tcpSocket;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class TcpEchoServer {
    private ServerSocket serverSocket = null;

    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        //不断与客户端建立连接
        while(true){
            //1.客户端发起连接,方法返回一个与客户端连接的Socket对象
            Socket socket = serverSocket.accept();
            //连接客户端,处理客户端发来的请求
            processConnection(socket);
        }
    }
    //===================
    //2.接收请求
    //3.解析请求,生成响应
    //4.返回响应
    // ===================
    private void processConnection(Socket socket) {
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()){
            //一次连接,客户端可能发送多次请求,使用while循环
            while(true){
                byte[] bytes = new byte[4096];
                int n = inputStream.read(bytes);
                //2.将二进制数据转换为字符串。n==-1代表已经读完请求数据
                String requestStr = new String(bytes,0,n==-1?bytes.length:n);

                //3.处理请求
                String responseStr = process(requestStr);

                //4.返回响应
                //响应二进制数据
                outputStream.write(responseStr.getBytes(),0,responseStr.getBytes().length);

                System.out.printf("[%s,%d] req:%s resp:%s",socket.getInetAddress(),socket.getPort(),requestStr,responseStr);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

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

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

}
4.4.3.2 TcpEchoClient
4.4.3.3 流套接字编程优化版

上述流套接字网络编程中,客户端与服务端,都用InputStream和OutputStream进行读写,而这两个IO流没有缓冲区,因此读写效率就会低不少。为了提高读写效率,用Scanner和PrintWriter分别对输入流和输出流套层壳:

服务端:

客户端:

4.4.3.4 最终版流套接字编程

学过多线程的,可能会意识到上述优化版服务端无法并发编程;若多个客户端同时发起连接,服务器只能建立一个连接,只有等第一个连接断开后,才能建立下一个客户端连接,如视频演示效果:

视频解析:最开始为了实现一个类多开,开启了"Allow....."。两个客户端同时连接,发送请求会出现,只有第一个客户端请求得到响应,其它客户端连接只能等前面的断开连接后才会连接上服务器

为了解决并发编程问题,使用线程池优化服务端连接代码:

最优版,服务端代码:(客户端无最优版)

java 复制代码
package tcpSocket;

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 TcpEchoServer2 {
    private ServerSocket serverSocket = null;

    public TcpEchoServer2(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        //不断与客户端建立连接
        while(true){
            //1.客户端发起连接,方法返回一个与客户端连接的Socket对象
            Socket socket = serverSocket.accept();
            ExecutorService executorService = Executors.newCachedThreadPool();
            executorService.submit(()->{
                processConnection(socket);
            });
        }
    }
    //===================
    //2.接收请求
    //3.解析请求,生成响应
    //4.返回响应
    // ===================
    private void processConnection(Socket socket) {
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()){
            System.out.printf("[%s:%d] 客户端上线!\n", socket.getInetAddress(), socket.getPort());
            //给inputStream套壳
            Scanner scanner = new Scanner(inputStream);
            //给outputStream套壳
            PrintWriter printWriter = new PrintWriter(outputStream);
            //一次连接,客户端可能发送多次请求,使用while循环
            while(true){
                if(!scanner.hasNext()){
                    System.out.printf("[%s:%d] 客户端下线\n",socket.getInetAddress(),socket.getPort());
                    break;
                }
                //2.接收请求
                String requestStr = scanner.next();

                //3.处理请求
                String responseStr = process(requestStr);

                //4.返回响应
                //响应二进制数据
                printWriter.println(responseStr);
                //要使用flush()刷新缓冲区,把数据从缓冲区中刷出,给到客户端。否则客户端收不到数据
                printWriter.flush();

                //打印日志
                System.out.printf("[%s,%d] req:%s resp:%s\n",socket.getInetAddress(),socket.getPort(),requestStr,responseStr);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

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

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

}

5. 总结

通过上述代码我们明白,使用操作系统提供的Socket API实现网络编程是非常简单的,但是上述代码创建的服务器只能自己访问,或被同一块区域网的设备访问(背后涉及到NAT)。

从TCP与UDP的四个特点对比和代码实现中,也就知道流套接字数据报套接字的区别(非常明显的是,一个Socket按字节传输,一个按数据报传输数据)。

相关推荐
北京聚信万通科技有限公司2 小时前
北京聚信万通科技有限公司获Odette CA官方授权,成为中国区“Odette ID及数字证书”官方注册审批管理机构
网络·科技·汽车·edi·电子数据交换·国产软件
hughnz2 小时前
钻井自动化案例研究
运维·自动化
ILL11IIL2 小时前
Docker容器技术
运维·docker·容器
蜡笔小新..2 小时前
Linux下Matplotlib使用Times New Roman字体的解决方案
linux·运维·matplotlib
飞yu流星2 小时前
文件压缩、文本内容、文本编辑
运维·服务器
2501_948114242 小时前
Claude Sonnet 4.6 深度评测:性能逼近 Opus、成本打骨折,附接入方案与选型指南
大数据·网络·人工智能·安全·架构
二宝哥2 小时前
Failed connect to mirrorlist.centos.org:80; Connection refused
linux·运维·centos
TOWE technology2 小时前
智能PDU——电力分配与数据信息的价值
网络·科技·pdu·智能pdu
humors2212 小时前
一些安全类网站(不定期更新)
linux·网络·windows·安全·黑客·白帽