网络编程【UDP数据报套接字编程】

目录

1.网络编程基础

1.1 为什么需要网络编程?

1.2 什么是网络编程

1.3 网络编程中的基本概念

2.Socket套接字

2.1 分类

3.UDP数据报套接字编程

3.1 DatagramSocket API

3.2 DatagramPacket API

3.3 基于 UDP socket 写一个简单的回显客户端服务器程序(一发一收)

3.3.1 UDP 服务端

3.3.2 UDP客户端

3.3.3 服务器和客户端它们的交互过程

3.3.4 整体效果演示 

3.3.5 一个服务器是可以同时给多个客户端提供服务的

3.4 写一个简单的单词翻译服务器(请求是一个英文单词,响应是这个单词的中文翻译)


 

1.网络编程基础

1.1 为什么需要网络编程?

用户在浏览器中,打开在线视频网站,如优酷看视频,实质是通过网络,获取到网络上的一个视频资源。

与本地打开视频文件类似,只是视频文件这个资源的来源是网络。相比本地资源来说,网络提供了更为丰富的网络资源:所谓的网络资源,其实就是在网络中可以获取的各种数据资源。

1.2 什么是网络编程

网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)

af40fea709e244d0b7bdd59f1bf67c9e.png

1.3 网络编程中的基本概念

1️⃣发送端和接收端

发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机

接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。

2️⃣请求和响应

一般来说,获取一个网络资源,涉及到两次网络数据传输:

第一次:请求数据的发送
第二次:响应数据的发送

3️⃣客户端和服务端

服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。

客户端获取服务的一方进程,称为客户端。

2.Socket套接字

Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。

2.1 分类

1️⃣流套接字:使用传输层TCP协议

有连接:使用 TCP 通信的双方,则需要刻意保存对方的相关信息
可靠传输
面向字节流:以字节为传输的基本单位,读写方式非常灵活
全双工:一条路径,双向通信

2️⃣数据报套接字:使用传输层UDP协议

无连接:使用 UDP 通信的双方,不需要可以保存对端的相关信息

不可靠传输

面向数据报:以一个 UDP 数据报为基本单位

全双工:一条路径,双向通信

  • 全双工:双向通信,一个管道,能 A->B,B->A 同时进行
  • 半双工:单向通信,一个管道,同一时刻,要么 A->B,要么 B->A ,不能同时进行

3.UDP数据报套接字编程

3.1 DatagramSocket API

Datagram——数据报

Socket——说明这个对象是一个 socket 对象(相当于对应到系统中一个特殊的文件(socket文件),socket 文件并非对应硬盘上的某个数据存储区域)

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

DatagramSocket 构造方法:
DatagramSocket() DatagramSocket() 创建一个 UDP 数据报套接字的 Socket,绑定到本机任意一个随机端口(一般用于客户端)
DatagramSocket(int port) DatagramSocket(int port) 创建一个 UDP 数据报套接字的 Socket,绑定到本机指定的端口(一般用于服务端)
DatagramSocket 方法:
void receive(DatagramPacket p) void receive(DatagramPacket p) 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)
void send(DatagramPacket p) void send(DatagramPacket p) 从此套接字发送数据报包(不会阻塞等待,直接发送)
void close() void close() 关闭此数据报套接字

3.2 DatagramPacket API

DatagramPacket是UDP Socket发送和接收的数据报。

DatagramPacket 构造方法:
DatagramPacket(byte[] buf, int length) 

DatagramPacket(byte[] buf, int length) 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length)

这个版本不需要“设置地址进去”,通常用来“接收消息

DatagramPacket(byte[] buf, int offset, int length,SocketAddress address) 

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

这个版本需要“显式的设置地址进去”。通常用来“发送消息

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

🌈构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建。

3.3 基于 UDP socket 写一个简单的回显客户端服务器程序(一发一收

回显服务器(echo server) 客户端发了一个请求,服务端返回一个一模一样的响应;一个服务器主要做三个核心工作:

1️⃣读取请求并解析

2️⃣根据请求计算响应(省略了)

3️⃣把响应返回到客户端

3.3.1 UDP 服务端

socket = new DatagramSocket(port);

1️⃣绑定一个端口 => 把这个进程和一个端口号关联起来

一个操作系统上面,有很多端口号,0 - 65535 。
 
程序如果需要进行网络通信,就需要获取到一个端口号,端口号相当于用来在网络上区分进程的身份标识符。(操作系统收到网卡数据,就可以根据网络数据报中的端口号,来确定要把这个数据交给哪个进程)

2️⃣分配端口号的过程:

手动指定:

new DatagramSocket(port);

系统自动分配:

socket = new DatagramSocket();

一个端口,在通常情况下,是不能被同一个主机上的多个进程同时绑定的;一个进程是可以绑定多个端口的。

如果端口已经被别人占用,再尝试绑定,就会抛出异常 throws SocketException

  • 读取客户端发来的请求,尝试读取,不是说调用了就一定能读到
//1.读取客户端发来的请求
socket.receive();

如果客户端没有发来请求,receive 就会阻塞等待,直到真的有客户端的请求过来了,receive 才会返回。

  • 对请求进行解析,把 DatagramPacket 转成一个 String
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
  • 根据请求,处理响应,虽然这里此处是个回显服务器,但是还是可以单独搞个方法来做这个事情
String response = process(request);

通过这个方法,实现根据请求计算响应,这个过程由于是回显服务器,所以涉及不到其他逻辑,但是如果是其他服务器,就可以在 process 里面,加上一些其他逻辑的处理

public String process(String req){
    return req;
}
  • 把响应构造成 DatagramPacket 对象(构造响应对象,要搞清楚,对象要发给谁,谁给咱发的请求,就把响应发给谁)
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());

这个也是构造 DatagramPacket 的一种方式,先是拿字符串里面的字节数组,来构造 Packet 的内容,还要把请求中的客户端地址拿过来,也填到包裹里去。
 
response.getBytes().length 可以写作 response.length 吗?
不行 response.getBytes().length 表示的是字节数,response.length 表示的是字符数

requestPacket.getSocketAddress() -> (地址)IP + 端口

  • 把这个 DatagramPacket 对象返回给客户端
socket.send(responsePacket);
System.out.printf("[%s:%d] req = %s; resp = %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);

 UDP 服务端总代码:

package Network;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: Lenovo
 * Date: 2023-04-02
 * Time: 16:05
 */

//UDP 服务端

public class UdpEchoServe {
    //需要先定义一个 socket 对象:通过网络通信,必须要使用 socket 对象
    private DatagramSocket socket = null;

    //绑定一个端口,不一定能成功
    //如果某个端口已经被别的进程占用了,此时这里的绑定操作就会出错
    //同一个主机上,一个端口同一时刻,只能被一个进程绑定
    public UdpEchoServe(int port) throws SocketException {
        //构造 socket 的同时,指定要关联/绑定的端口
        socket = new DatagramSocket(port);

    }

    //启动服务器的主逻辑
    public void start() throws IOException {
        System.out.println("服务器启动!");
        //一旦服务器已启动,调用 start 方法,就会立即执行到 receive;
        //此时,还没有客户端达赖数据,此时 receive 就会阻塞等待,直到传输过来数据
        while (true) {
            //每次循环,要做三件事:
            //1.读取请求并解析(构造空饭盒)
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);//空的对象,空的对象持有了一个空的字节数据

            //在Java中,[] 和 . 是解引用;直接 = 不是,内部要想影响到外部,就需要通过解引用进行的
            //receive 参数类型是 DatagramPacket,是引用参数,即receive 方法内部,针对参数进行修改,外部也生效
            socket.receive(requestPacket);
            //为了方便处理这个请求,把数据报转换成 String
            //这个操作不是必须的,只是此处为了后续代码简单,就简单构造一个String,拿着requestPacket 中持有的字节数组进行构造
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());

            //2.根据请求计算响应(此处省略这个步骤)
            String response = process(request);
            //3.把响应结果写回到客户端:根据 response 字符串,构造一个 DatagramPacket
            //和请求 packet 不同,此处构造相应的时候,需要指定这个报要发给谁
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                    requestPacket.getSocketAddress());
            socket.send(responsePacket);
            System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(),
                    requestPacket.getPort(), request, response);
        }
    }

    //这个方法希望是根据请求计算响应
    //由于写的是一个 回显 程序,请求是啥,响应是啥
    //如果后续写个别的服务器,不再回显了,而是有具体的业务了,就可以修改 process 方法,根据需要来重新构造响应
    //之所以单独列成一个方法,这是一个服务器中的关键环节
    private String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        UdpEchoServe udpEchoServe = new UdpEchoServe(9090);
        udpEchoServe.start();

    }
}

3.3.2 UDP客户端

服务器,端口一般是手动指定的,如果自动分配,客户端就不知道服务器的端口是啥了,因此服务器有固定端口客户端才好访问。

客户端,端口一般是自动分配的,客户端程序是安装在用户的电脑上的,用户电脑当前运行哪些程序,是不可控的,如果要是手动指定端口,说不好这个端口就和其他程序冲突了,导致咱们的代码无法运行。

public UDPEchoClient() throws SocketException {
    //客户端的端口号,一般都是由操作系统自动分配的,虽然手动指定也行,习惯上还是自动分配比较好
    socket = new DatagramSocket();
}
  • 让客户端从控制台获取一个请求数据
System.out.println("> ");
String request = scanner.next();
  • 把这个字符串请求发送给服务器,构造 DatagramSocket,构造的 Packet 既要包含 要传输的数据,又要包含把数据发送到哪里(另外一种 DatagramPacket 的构造方法)
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName("127.0.0.1"),8000);

InetAddress.getByName(“127.0.0.1”) : 通过这个字符串来构造的 InetAddress,此处的 127.0.0.1 回环 IP 就表示当前主机。
8000  : 服务器端口号

这个包裹,就是要从客户端发送给服务器,就需要知道,发送的内容,以及发送的目的地是哪里(收件人地址 + 端口)

  • 把数据报发送给服务器
socket.send(requestPacket);
  • 从服务期读取响应数据
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
  • 把响应数据获取出来,转成字符串
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.printf("req: %s;resp: %s\n",request,response);

UDP 客户端总代码:

import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIP;
    private int serverPort;

    // 客户端启动, 需要知道服务器在哪里!!
    public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
        // 对于客户端来说, 不需要显示关联端口.
        // 不代表没有端口, 而是系统自动分配了个空闲的端口.
        socket = new DatagramSocket();
        this.serverIP = serverIP;
        this.serverPort = serverPort;
    }

    public void start() throws IOException {
        // 通过这个客户端可以多次和服务器进行交互.
        Scanner scanner = new Scanner(System.in);
        while (true) {
            // 1. 先从控制台, 读取一个字符串过来
            //    先打印一个提示符, 提示用户要输入内容
            System.out.print("-> ");
            String request = scanner.next();
            // 2. 把字符串构造成 UDP packet, 并进行发送.
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIP), serverPort);
            socket.send(requestPacket);
            // 3. 客户端尝试读取服务器返回的响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            // 4. 把响应数据转换成 String 显示出来.
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            System.out.printf("req: %s, resp: %s\n", request, response);
        }
    }

    public static void main(String[] args) throws IOException {
        // UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 9090);
        UdpEchoClient udpEchoClient = new UdpEchoClient("42.192.83.143", 9090);
        udpEchoClient.start();
    }

}

3.3.3 服务器和客户端它们的交互过程

1️⃣服务器先启动,执行到 receive 进行阻塞

bc4815f807b349a6897f30b02dd48a8d.png

2️⃣由于阻塞,服务器接下来的代码就不执行了,此时执行客户端:客户端通过 Scanner 读取请求执行 send 操作

4eb12cf201fb4c259372e76014524500.png

 3️⃣此时,客户端和服务器都会往下执行代码:客户端执行 receive 读取响应,会阻塞等待;服务器从 receive 返回,读到请求数据(客户端发来的),执行到 process 生成响应,执行到 send 发送请求,并且打印日志

007eedc0a5f34b56850bbc83d7fafb89.png

972c89ad417d40a8b610998087549b5c.png

 4️⃣服务器进入下一轮循环,再次阻塞在 receive,等待客户端下一次请求;客户端真正收到服务器 send 回来的数据后就会解除阻塞,执行下边的打印日志

3ea734a3db104243bdb8ce9e9676d4a9.png

4042122fe06243de9b137a68d1f68e6b.png

 4️⃣客户端继续进行下一轮循环,阻塞在 Scanner. next 这里等待用户输入新的数据

b346b6a9305f440f97650308430916bc.png

3.3.4 整体效果演示 

  • 先启动服务器,再启动客户端

客户端中输入一个hello:

bb3e5e8c840e4e429ddfc6d4457b31db.png

在服务器中:

ba3587a5c2d34d4f8d2704d356271bad.png 

继续在客户端中输入一个你好:

9215b973d9154c6187c6e42a72a1c5c9.png 

服务器中显示:

19395b5da2ea49f7bbee724001bc8afa.png 

3.3.5 一个服务器是可以同时给多个客户端提供服务的

在 idea 中启动多个客户端,需要配置,默认一个程序只能启动一个

如在 IDEA 中,你想打开多个客户端,你发现你再运行一次客户端,就会把之前的客户端给关闭了,此时我们需要设置一下,就可以启动多个客户端。

b19d12f8a5794e5da0721aec0f671f67.png

4d00fc86c69a4d75849af275fa8cb51d.png

 此时我们就可以打开多个客户端了

3.4 写一个简单的单词翻译服务器(请求是一个英文单词,响应是这个单词的中文翻译)

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;

//字典服务器 / 翻译服务器
    //希望实现一个英译汉的效果
    //请求的是一个英文单词,响应是对应的中文翻译
public class UDPDicServer extends UDPEchoServer{
    private Map<String, String> dic = new HashMap<>();

    public UDPDicServer(int port) throws SocketException {
        super(port);

        //这里的数据可以无限的构造下去
        //即使是有道词典这种,也是类似的方法实现(打表)
        dic.put("cat","小猫");
        dic.put("dog","小狗");
        dic.put("fuck","卧槽");
    }

    //和 UDPEchoServer 相比,只是 process 不同,就重写这个方法即可
    public String process(String req){
        return dic.getOrDefault(req,"这个词俺也不会!");
    }

    public static void main(String[] args) throws IOException {
        UDPDicServer server = new UDPDicServer(8000);
        server.start();
    }
}

a4c7c1a012144af9af57504856b8bf04.png

 

 

 

 

 

  • 28
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 19
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

奋斗小温

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值