网络编程套接字 socket 之 UDP

一、网络编程

        我们网络编程的核心: Socket API,操作系统为我们应用程序提供的API,我们的Socket是和传输层密切相关的。Socket API 套接字,翻译是插座

        传输层为我们提供了两个最核心的协议UDP/TCP,因此Socket API也为我们提供了两种风格TCP/UDP。(有第三种unix早就没人用了)

简单认识一下TCP/UDP:
        TCP 有连接   可靠传输   面向字节流 全双工
        UDP 无连接 不可靠传输 面向数据报 全双工


有链接/无连接:

        打电话就是有连接的,需要连接建立了才能通信,连接建立需要双方来接受

        发微信短信就是无连接的,直接发消息,不管对方要不要连接


可靠/不可靠传输:

        发送方可以知道自己的消息是不是发过去了,还是丢了。

        打电话是可靠传输

        发微信短信就是不可靠传输,带已读功能的可以视为可靠传输,像QQ,wx这种没有已读功能的就可以视为不可靠传输

注意:可不可靠和有没有连接没有任何关系


面向字节流:数据传输就和文件读写类似,“流式”的

面向数据报:数据传输则以一个个的“数据报”为单位(一个数据报可能是若干个字节,带有一定的格式的)


全双工:一个通信通道,可以双向传输,也就是说,既可以发送,也可以接收

TCP特点:

    使用TCP协议,必须双方先建立连接,它是一种面向连接的可靠通信协议
    传输前,采用”三次握手"方式建立连接,所以是可靠的
    在连接中可进行大数据量的传输
    连接、发送数据都需要确认,且传输完毕后,还需释放已建立的连接,通信效率低

应用场景:对信息安全要求较高的场景,例如:文件下载、金融等数据通信


UDP特点:

    UDP是一种无连接,不可靠传输协议
    将源IP、目的IP和端口封装成数据包,不需要建立连接
    每个数据包大小限制在64kb内
    发送不管对方是否准备好,接收方收到也不确认,所以是不可靠的
    可以广播发送,发送数据结束时无需释放资源,开销小,速度快
    应用场景: 语音通话,视频会话等

二、UDP数据报套接字编程

DatagramSocket:

        DatagramSocket  这个类表示一个 Socket 对象,操作系统操作网卡,不是直接操作的,而是把网卡抽象成了特殊的文件,称为 socket 文件,相当于是文件描述符表上的一项。

        普通的文件对应的硬件设备是硬盘,socket文件对应的硬件设备是网卡
        一个 Socket 对象就可以与另一台主机进行通信了,如果要和多个不同的主机通信,就需要创建多个 Socket 对象

        receive 方法参数传入的是一个空的 DatagramPacket 对象,receive 方法内部会对这个对象进行填充,从而构造出结果数据,我们称这样的参数为输出型参数

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

DatagramPacket

        DatagramPacket 是 UDP socket 进行发送和接收的数据报,表示 UDP 中传输的一个报文,构造这个对象,可以指定一些具体的字节数组进去,作为持有数据的缓冲区。

DatagramPacket的一些方法:

三、实现客户端服务器程序

        普通服务器做的工作:收到请求,根据请求计算响应,返回响应,最重要的环节就是计算响应这一部分。

回显服务器(echo server)省略了最重要的环节 "计算响应“ 这一部分,接收到什么就返回什么。代码没有实际的业务,没有太大作用和意义,只是展示socket api的基本用法。

作为一个真正的服务器,一定“根据请求计算响应”这个环节是最最最重要的。

        对于服务器来说,创建socket对象的同时,要让他绑定上一个具体的端口号

EchoServer 回显服务器

第一步:创建一个socket对象

我们网络编程,本质上是要操作网卡。

但是网卡不方便我们直接操作,于是我们操作系统内核中,使用“socket"这样的文件来抽象表示网卡,所以我们要想进行网络通信,首先得有一个socket对象。

public class EchoServer {
    private DatagramSocket socket = null;

    public UdpEchoServer() throws SocketException {
        socket = new DatagramSocket();
    }
}

第二步:为服务器绑定一个 具体的端口号

我们的服务器,在真正创建对象的时候,需要绑定一个具体的端口号,为啥是具体的呢?

服务器是网络传输中,是属于比较被动的一方,如果让操作系统随机分配端口,那么客户端就不知道服务器端口是哪个了 ,也就无法进行通信了。

如果把 服务器的端口号 比作 食堂商家的窗口号 ,作为食堂商家就需要一个具体的窗口号为食客服务,在计算机中也是如此,服务器必须要有一个具体的端口号为客户端进行服务。

public EchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

第三步: 启动客户端

读取客户端发来的请求是啥

由于服务器不只是为一个客户端提供服务就完了,需要服务很多的客户端

为什么  while(true)死循环:因为服务器需要 7*24 小时运行

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);
        }
    }

DatagramSocket 这个类的receive能阻塞,是因为操作系统原生提供的API(recv)就是阻塞的函数,这里的阻塞不是Java实现的,而是操作系统内核里实现的。


第四步:构造字符串

我们的UDP传输的基本单位是 DatagramPacket.

此时我们服务器接收到的 DatagramPacket 是一个特殊的对象,并不方便我们直接进行处理,我们可以把这里包含的数据拿出来,构造成一个字符串。 

String request = new String(requestPcket.getData(),0, requestPcket.getLength());

         我们在创建DatagramPacket给的最大长度是4096,但我们实际可能只用了一小部分,因此我们在构造字符串时,通过getLength()获取数据报实际的长度,只把实际有效的部分构造成字符串即可。


第五步:处理请求—— process方法
        我们获取到了客户端的请求之后然后我们对请求进行处理(根据请求计算响应),我们这里是 回显服务器,响应和请求相同,实现的是接收什么,回应什么。 

 process方法表示“根据请求计算响应”,这里是回显服务器,响应和请求相同,实现的是接收request,就回应request。 

public String process(String request) {
        return request;
    }

第六步: 响应请求—— send方法

        然后我们将这个响应发送给客户端,需要用到 send 方法,send 的参数也是 DatagramPacket ,那么在send前,我们就需要先构造出 DatagramPacket 对象。

        此处构造的响应对象,不能是用空的字节数组构造了,而是要使用响应回来的数据来构造。

DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length);

因为我们的DatagramPacket不认字符只认字节,所以我们将response转换为字节数组

response.getBytes()拿到字节数组,response.getBytes().length获取的是字节的个数。

答案是不可以,因为response.length()获取的是字符的个数,response.getBytes().length获取的是字节的个数。
我们这里的DatagramPacket的构造还是有一点点问题,我们这里的数据是构建好了,那给谁发呢?

DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPcket.getSocketAddress());

我们在参数中,应该传入客户端的地址信息。

然后进行发送 ->

socket.send(responsePacket);

数据到达网卡,经过内核层层分用,最终到达了UDP传输层协议,调用 receive 相当于是执行内核中udp相关的代码,将UDP数据报的载荷取出来,拷贝到用户提供的byte[] 数组中。

 //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);//一定是服务器先启动执行,运行到receive后阻塞

            //此时这个 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();
    }
}

四、客户端:

我们一次网络通信涉及到五元组:
源IP,源端口,目的IP,目的端口,协议类型。

构造客户端Socket对象时,不需要显式的去绑定一个端口,而是由系统分配一个空闲端口。

服务器的端口:需要固定指定,为了方便客户端找到服务器程序。
客户端的端口:由系统自动分配的,如果我们手动指定,可能会与客户端其他程序的端口冲突
为什么服务器不怕冲突?
因为服务器上面的程序可控,而客户端是运行在我们用户电脑上,环境复杂,更不可控。


第一步:创建Socket对象

首先我们需要创建一个Socket对象,并且获取配置服务器的ip和端口号 

private DatagramSocket socket = null;
    private String serverIp = null;//服务器的ip 
    private int serverPort = 0;//服务器端口号 
    
    public EchoClient(String serverIp,int serverPort) throws SocketException {
        socket = new DatagramSocket();
        this.serverIp = serverIp;
        this.serverPort = serverPort;
    }

 第二步:从控制台接收我们需要发送端数据。

System.out.print("> ");
            String request = scan.next();
            if(request.equals("exit")) {
                System.out.println("客户端退出");
                break;
            }

 第三步:构造字符串

 我们将request字符串构造成DatagramPacket进行发送。

DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName(serverIp),serverPort);

我们在构造DatagramPacket的时候,需要将ip和端口号都传入,此处需要传入的IP是32位的整数形式,但我们这里的ip是字符串,所以需要使用InetAddress.getByName进行转换,然后进行发送。

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.println(response);


客户端完整代码: 

//UDP 版本的回显客户端
public class UdpEchoClient {
        private DatagramSocket socket = null;
        private String serverIp = null;
        private int serverPort = 0;

        //一次通信,需要有两个ip ,两个端口
        //客户端的IP 是已知的 127.0.0.1
        //客户端的port是系统自动分配的
        //服务器IP和端口 也需要告诉客户端,才能顺利把消息发给服务器
        public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
                socket = new DatagramSocket();
                this.serverIp = serverIp;
                this.serverPort = serverPort;
            }

        public void start() throws IOException {
            System.out.println("客户端启动!");
            Scanner scan = new Scanner(System.in);
            while (true) {
                //1、从控制台读取要发送的数据
                System.out.print("> ");
                String request = scan.next();
                if(request.equals("exit")) {
                    System.out.println("客户端退出");
                    break;
                }

                //2、构造成UDP请求,并发送
                //构造这个 Packet 的时候,需要把serverIp和port 都传入过来,但是此处IP地址需要填写的是一个32位的整数形式
                //上述的IP地址是一个字符串,需要使用 InetAddress.getByName 来进行一个转换
                DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
                        request.getBytes().length, InetAddress.getByName(serverIp),serverPort);
                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());

                //把解析好的结果显示出来
                System.out.println(response);
            }
        }
    public static void main(String[] args) throws IOException {
        UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
        client.start();
    }
} 

 

        客户端服务器程序,一个服务器是给许多客户端提供服务的,但是我们IDEA默认只能启动一个客户端,我们需要手动设置一下。

 

现在我们就可以创建多客户端与服务器进行通信了。


这里的报错是 端口冲突
也就是说 一个端口只能被一个进程使用,如果有多个使用就不行。  

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值