UDP数据报套接字编程(网络编程)

一.引入

现在我们正处于万物互联的时代,在以后的编程中是离不开网络的,所以我们有必要学习一下网络编程,而网络编程的核心是Socket Api(网络编程套接字),在网络协议的分层中我们知道,传输层以下的协议,都是操作系统内核实现的,换句话说,Socket Api是站在传输层的角度和应用层进行交互的,也可以认为Socket Api是和传输层密切相关的,而传输层中提供了两个最核心的协议->TCP/UDP,因此Socket Api也提供了两种不同的风格,那就是TCP/UDP风格(其实还有第三种风格unix域套接字,只不过现在不会使用了),要想知道Socket Api如何实现网络编程,那么先来简单了解一下TCP/UDP

二.简单了解TCP/UDP特性

1. UDP特性

简单用几个词介绍UDP特性:无连接,不可靠传输,面向数据报,全双工

无连接:不需要对方做出响应,直接发送即可
如:发短信,发微信

不可靠传输:只管发消息,不知道消息是被接收到了,还是丢包了,消息到不到UDP不管
如:发短信,发微信

面向数据报:数据传输的基本单位是数据报的整数倍,一个数据报可能是多个字节

全双工:一个通信通道,可以双向传输(与之对应的是半双工)

2.TCP特性

同样简单用几个词介绍TCP特性:有连接,可靠传输,面向字节流,全双工

有连接:等待对方做出响应,才开始发送信息
如:打电话

可靠传输:自己清楚消息是否被对方接受到,尽可能把数据传输到
如:有些软件具有已读功能,或者是打电话都是可靠传输

面向字节流:数据传输的基本单位是字节流,这里和IO流概念相同,可以一次读取一些分多次读完

全双工:一个通信通道,可以双向传输(与之对应的是半双工)

注意

网络环境是天然复杂的,我们在传输时并不能保证数据会100%被送达,不要将可靠传输理解为数据会被100%送达

三.Socket Api为UDP提供的两(三)个类

1.DatagramSocket

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

我们用DatagramSocket来表示一个socket对象,在操作系统中,也是将socket对象当作文件处理的,体现了操作系统在处理时一切皆文件的思想

只不过普通的文件对应的硬件设备是硬盘,socket文件对应的硬件设备是网卡

一个socket对象可以和一台主机通信,如果想和多台不同的主机通信,就需要多个socket对象

(1)DatagramSocket的构造方法

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

第一个是无参的构造方法,此时是操作系统随机分配一个没有被占用的端口号,一般用于客户端

第二个是有参数的构造方法,需要传入一个端口号,也就是说让这个socket对象和这个指定的端口关联起来.

本质上说不是进程和端口建立联系,而是进程中的socket对象和端口建立联系

(2)DatagramSocket的方法

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

receive和send这两个方法,主要看参数,参数类型是下面的类,即DatagramPacket
send构造好后直接发送就可以,而receive时DatagramPacket p必须是一个空的对象,receive方法内部会对这个空对象进行内容填充,从而构造出一个结果数据,此时这个参数也被称为"输出型参数"

close()方法就是用完记得释放资源

2.DatagramPacket

DatagramPacket是UDP Socket发送和接收的数据报,即UDP传输中的一个报文

(1)DatagramPacket的构造方法

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

因为DatagramPacket是UDP传输中的一个报文,所以我们需要构造一些具体的数据进去

DatagramPacket(byte[] buf, int length)相当于是把buf缓冲区的数据添加进去

DatagramPacket(byte[] buf, int offset, int length,SocketAddress address)既构造了缓冲区又构造了地址,其中SocketAddress这个类表示IP+端口号

(2)DatagramPacket的方法

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

以上三个方法相当于是把报文中的数据取出来

3.InetSocketAddress

InetSocketAddress(SocketAddress)的子类

(1)InetSocketAddress的构造方法

方法签名方法说明
InetSocketAddress(InetAddress addr, int port)创建一个Socket地址,包含IP地址和端口号

四.通过UDP套接字编程实现回显服务器(echo server)

1.什么是回显服务器

要想知道回显服务器,先来看看普通服务器

一个普通的服务器有三大步骤:

(1)收到请求
(2)根据请求计算响应
(3)返回响应

而echo server则省略了第二步根据请求计算响应,也就是说收到什么就返回什么

这个代码没有具体的业务逻辑,也没什么作用和意义,就是向大家展示一下,socket Api的基本用法

作为一个服务器最重要的就是根据请求计算响应

2.服务器端代码实现

package network;

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

//UDP 版本的回显服务器
public class UdpEchoServer {
    //网络编程,本质上是要操作网卡
    //但是网卡不方便直接操作,在操作系统内核中,使用了一种特殊的叫做"socket"这样的文件来抽象表示网卡
    //因此进行网络通信势必要先有一个socket对象
    private DatagramSocket socket = null;

    //对于服务器来说,创建 socket对象的同时,要让他绑定一个具体的端口号
    //服务器一定要关联上一个具体的端口!!!
    //服务器是网络传输中,被动的一方,如果是操作系统分配的端口(每次启动服务器都不同),此时客户端就不知道这个端口是啥了,也就无法进行通信了!!!
    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动!!!");
        //服务器不是只给一个客户端提供服务就完了,需要服务很多客户端
        while(true)
        {
            //只要有客户过来就可以提供服务
            //1.读取客户端发来的请求是啥
            //receive 方法的参数是一个输出型参数,需要先构造好一个空白的DatagramPacket对象,交给receive来填充
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);
            //此时这个DatagramPacket是一个特殊的对象,不方便进行处理,所以要将其转化为字符串以方便我们后续操作
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());
            //2.根据请求计算响应,由于此处是回显服务器,响应和请求相同
            String response = process(request);
            //3.把响应写回到客户端,send的参数也是DatagramPacket,需要把这个Packet对象构造好
            //此处构造的响应对象,不能是用空的字节数组构造了,而是要使用响应数据来构造
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
            socket.send(responsePacket);
            //4.打印一下,当前这次请求响应的处理中间结果
            System.out.printf("[%s:%d] req: %s; resp: %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);
        }
    }

    //这个方法就表示根据请求计算响应
    public String process(String request)
    {
        return request;
    }

    public static void main(String[] args) throws IOException {
        //端口号在1024-65535之间任意选一个
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }
}


具体的都在代码的注释中说明了,再说一下需要注意的

(1)服务器代码中的注意事项

在这里插入图片描述
1.1 对于服务器来说,他的端口号不能让操作系统随机指定,否则下次再启动程序,又会重新分配端口,两次端口不一样,对于用户来说他是不知道下一次的端口变为多少的,也就无法连接到服务器了。

在这里插入图片描述
1.2 这就是上文提到的receive方法,他是输出型参数,即通过DatagramPacket包装的byte数组是空的,只是将这个壳子传给receive,这个方法内部会自动将内容填充,客户端将报文发送到网卡中,在经过层层分用到达传输层UDP协议,调用receive相当于执行到了传输层UDP中的相关代码,在把内容填充到buf中

在这里插入图片描述
1.3 这里注意,因为DatagramPacket对象不好进行处理,所以我们需要将他转为字符串后在对其请求计算响应,但我们在创建buf数组时指定的长度是4096,实际上这些空间并不会被全部使用,我们在转换时只用数据的真实长度

在这里插入图片描述
1.4这句代码切记,第二个参数不能写成response.length(),因为response.length()代表的是字符串长度,而不是字节长度,DatagramPacket在处理时,是以字节处理的,如果该字符串里的字符是Ascill码里的字符,则只占一个字节,此时两者相同,一旦包含中文,则response.length()<response.getBytes().length。

1·5代码执行一次while循环就处理一次请求-响应,但是客户端啥时候发请求???有多少个发请求???我们都不知道,所以为了解决这个问题循环的次数要和客户端发的请求步调一致,步调一致怎么实现呢,其实就是靠receive方法实现阻塞等待客户端发送请求,如果客户端发送了请求就能顺利读出来,如果没法请求,程序就会阻塞,等待客户端发送请求,类似于Scanner操作

1.6上面1.5提到有多少个客户端发送请求就要循环多少次,那么如果客户端一直不停的发送请求且请求又快又多,那么是否while循环就处理不过来了???答案是肯定的,那么如何解决呢(以上问题被称为高并发)
(1)利用多线程,多个线程同时处理客户端请求,更充分的利用计算机硬件资源,但硬件资源也是有限的,当我们的CPU达到瓶颈,再增加线程也无济于事,此时需要第二种方法
(2)增加机器数量,但会提高管理成本
(3)分布式,管机器

3.客户端代码实现

package network;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serveIP = null;
    private int servePort = 0;
    //一次通信需要两个IP和两个端口
    //客户端的IP是127.0.0.1(已知)
    //客户端的Port是系统自动分配的
    //服务器的IP和Port也需要告诉客户端,才能把信息发给服务器
    public UdpEchoClient(String serveIP,int servePort) throws SocketException {
        socket = new DatagramSocket();
        this.serveIP = serveIP;
        this.servePort = servePort;
    }
    public void start() throws IOException {
        System.out.println("客户端启动!!!");
        Scanner sc = new Scanner(System.in);
        while(true)
        {
            //1.从控制台读取要发送的数据
            System.out.println(">");
            String request = sc.next();
            if(request.equals("exit")){
                System.out.println("Goodbye");
                break;
            }
            //2.构造成UDP请求,并发送
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(serveIP),servePort);
            socket.send(requestPacket);
            //3.读取服务器的UDP响应,并解析
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(),0,responsePacket.getLength());
            //4.把解析好的数据显示出来
            System.out.println(response);
        }
    }

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

}


(1)客户端代码中的注意事项

在这里插入图片描述
1.1可以看到客户端的构造方法中不需要指定具体的端口,而是通过操作系统自动分配一个没有被占用的,这是因为对于有些用户来说,他们不清楚自己的电脑中正在运行的进程的端口是多少,如果自己指定的端口和正在运行的进程的端口冲突,那么会导致客户端程序无法启动,而服务器自己指定是因为程序员可以通过某些指令查看自己哪些端口是空闲的,从而指定端口。

简单地说就是在同一个主机上一个端口不能同时被多个进程使用,但一个进程可以绑定多个端口号(进程只要创建多个socket对象就可以绑定多个端口)准确的说,socket和端口是一对一的,进程和socket是一对多的

端口冲突引起的异常如下:
在这里插入图片描述

五.代码的执行顺序

在这里插入图片描述
在这里插入图片描述
上述服务器和客户端都有一个第四步,表明这两步在时序上是同时进行的

六.程序运行效果

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
IDEA默认只能打开一个线程,要想同时打开多个客户端(多个进程),要进行如下设置
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

七.总结

(1)注意我们当前的执行只是在本机上运行,而网络存在的意义是实现跨主机通信,其实我们的程序现在已经具备这个功能,但仍需中间介质
解决方法:
(1)两人在同一个局域网中(没有外网IP)
(2)将程序打包放到云服务器上(有外网IP,而我们自身的电脑并没有)

通过上述例子我们需要知道服务器和客户端的工作流程
服务器:
(1)读取请求并解析
(2)根据请求计算响应
(3)构造响应并写回给客户端
其次对于服务器来说,端口必须是确定好的

客户端
(1)从控制台读取要发送的数据
(2)构造成UDP请求并发送
(3)读取服务器的UDP响应,并解析
(4)把解析好的结果显示出来
对于客户端来说,端口是操作系统分配的

  • 12
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 15
    评论
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值