2.6.2 网络套接字章 UDP服务器客户端

1.0 两个核心类

  • 由两者主要的核心类 DatagramSocket 和 DatagramPacket

2.0 DatagramSocket API

DtatgramSocket 概念

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

  • 操作系统用文件这样的概念,来管理一些软硬见资源,比如把键盘插到电脑上,键盘会被当做文件来处理,操作系统也是使用文件的方式来管理网卡的,表示网卡的这类文件我们就称为Socket文件

  • java 中的 socket对象,就对应系统里的socket文件(最终会落到网卡上,java做了封装)

  • 要进行网络通信,就必须得先有 socket 对象

  • DtatgramSocket,这是一个Socket对象

DtatgramSocket 构造方法

方法签名方法说明
DatagramSocket()创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口,系统自动分配端口 (一般用于客户端)
DatagramSocket(int port)创建一个UDP数据报套接字的Socket,绑定到本机指定的端口,服务器一般指定端口**(一般用 于服务端)**
  • 服务器需要一个固定的端口号,方便其他客户端找过来,因此需要固定端口号
  • 客户端就像去餐厅点餐,点完后随便坐,原来的位置可能被别人做了,因此客户端口就不能固定端口,os会自动分配空闲端口号
  • 服务器是完全掌握在设计设手里,设计者可以把服务器里的多个程序都安排好,让他们用不同的端口

DtatgramSocket 相关方法

方法签名方法说明
void receive(DatagramPacket p)从此套接字接收数据报(如果没有接收到数据报,该方法会阻 塞等待),读
void send(DatagramPacket p)从此套接字发送数据报包(不会阻塞等待,直接发送),写
void close()关闭此数据报套接字
  • UDP 的基本单位是数据报,所以传输接收的对象都是DatagramPacket

3.0 DatagramPacket API

DatagramPacket 概念

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

  • 表示了一个UDP数据报.

  • 代表了系统中设定的 UDP 数据报的二进制结构 (java做了封装)

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 数据报,必然要能够承载一些数据,通过指定的 byte[] 作为存储空间.
  • SocketAddress address 对象包含了IP和端口号

DatagramPacket 方法

方法签名方法说明
InetAddress getAddress()从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取 接收端主机IP地址
int getPort()从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获 取接收端主机端口号
byte[] getData()获取数据报中的数据,获取到UDP数据报的载荷部分,(完整的应用层数据报)

4.0 InetSocketAddress API

  • 构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创 建。
  • 这个类和DatagramPacket 严格绑定

InetSocketAddress ( SocketAddress 的子类 )构造方法:

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

5.0 UDP服务器

当前实现的服务器

  • 简单的回显服务器(echo server 客户都安发啥,服务端就返回啥)

服务器如何反应客户端

  • 一个服务器要给很多客户端提供服务
  • 一个服务器不知道客户端啥时候来,所以反复的, 长期的执行针对客户端请求处理的逻辑(while)

服务器三个核心:

  • 一个服务器, 运行过程中, 要做的事情, 主要是三个核心环节
  1. 读取请求, 并解析
  2. 根据请求, 计算出响应**(复杂服务器和简单服务器的主要区别,回显服务器第二步基本没有)**
  3. 把响应写回给客户端

输出型参数

  • 传入某个方法的参数是空的,当方法调用结束后,这个传入的空对象就会被填满

代码示例

服务器1

回显服务器(无事务,收到啥返回啥)

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

// UDP 的 回显服务器.
// 客户端发的请求是啥, 服务器返回的响应就是啥.
public class UdpEchoServer {
    //数据报对象
    private DatagramSocket socket = null;


    // 参数是服务器要绑定的端口,构造方法,port时端口号
    public UdpEchoServer(int port) throws SocketException {//网络编程一个常见的异常
        socket = new DatagramSocket(port); //端口号占用了则会new异常
    }


    // 使用这个方法启动服务器.
    public void start() throws IOException {//网络通信也是一种IO也会抛出IOException
        System.out.println("服务器启动!");//日志,证明启动了

        //一个服务器要给很多客户端提供服务
        //一个服务器不知道客户端啥时候来,所以反复的, 长期的执行针对客户端请求处理的逻辑(while)
        while (true) {
            // 反复的, 长期的执行针对客户端请求处理的逻辑.


            // 一个服务器, 运行过程中, 要做的事情, 主要是三个核心环节.


            // 1. 读取请求, 并解析
            //new一个用于读取请求的对象,创建此对象就要手动申请好内存空间(不能超过64KB),不像我们我们学过的集合类,内部有自己管理内存的能力
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            //DatagramPacket 是输出型参数,传入receive的对象是空,调用完receive后的空对象的内容就会被填充上
            //如果客户端的请求一直没来,recieve方法就会阻塞等待==>一直阻塞到真的有客户端发送东西来
            socket.receive(requestPacket);



            //把得到的数据报,转化成字符串,这样的转字符串的前提是, 后续客户端发的数据就是一个文本的字符串.
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());



            // 2. 根据请求, 计算出响应
            //回显服务器这请求就是响应,因此假装调用下自己写的一个方法
            String response = process(request);



            // 3. 把响应写回给客户端
            // 此时需要告知网卡, 要发的内容是啥(获取response的内部字节数组和长度,长度要是字节长度), 要发给谁(客户端的IP和端口在requestPacket数据报里面).
            //socket api 本生都是按照字节来处理的,一定要传字节数组与字节长度
            //DatagramPacket 这个对象 就包含双方通信的ip 和 port
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                    requestPacket.getSocketAddress());
            socket.send(responsePacket);//用send返回响应数据报



            // 记录日志, 方便观察程序执行效果.
            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 {
        UdpEchoServer server = new UdpEchoServer(9090);//端口号随便写的
        server.start();//启动服务器
    }
}

服务器2

继承回显服务器父类,添加翻译功能的服务器

//实现翻译的服务器
//直接继承父类的代码
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;

public class UdpDictServer extends UdpEchoServer {
    private Map<String, String> dict = new HashMap<>();

    public UdpDictServer(int port) throws SocketException {
        super(port);//帮助父类构造,一定要在构造的第一行

        dict.put("cat", "小猫");
        dict.put("dog", "小狗");
        dict.put("fuck", "卧槽");
        // 可以在这里继续添加千千万万个单词. 使每个单词都有一个对应的翻译.
    }

    // 是要复用之前的代码, 但是又要做出调整.
    @Override
    public String process(String request) {
        // 把请求对应单词的翻译, 给返回回去.
        return dict.getOrDefault(request, "该词没有查询到!");//后面是没查到返回的默认结果
    }

    public static void main(String[] args) throws IOException {
        //多态,父类继承子类
        UdpDictServer server = new UdpDictServer(9090);
        // start 不需要重新再写一遍了. 直接就复用了之前的 start !
        server.start();
    }
}

注意

Q: DatagramSocket 是否需要close

A1:仅限于只有一个socket对象,并且生命周期很长的时候

  • 对于服务器来说,DatagramSocket 不关闭,问题不大
  • 整个程序中只有这一个socket对象,不是很频繁创建的
  • 整个对象的生命周期非常长,跟随整个程序的,此时socket 就需要保持打开的状态
  • socket对象 => 系统中的socket 文件 => 文件描述符(最主要的目的是为了释放文件描述符,才要关闭socket对象的)
  • 而进程结束就把pcb回收了,里面的文件描述符表都销毁了,所以没必要每次都close DatagramSocket

A2: 如果有多个socket对象, socket对象生命周期很短的时候,需要频繁创建和销毁

  • 一定要close,不然文件描述符会炸

6.0 UDP 客户端

代码示例


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

// UDP 的 回显服务器.
// 客户端发的请求是啥, 服务器返回的响应就是啥.
public class UdpEchoServer {
    //数据报对象
    private DatagramSocket socket = null;


    // 参数是服务器要绑定的端口,构造方法,port时端口号
    public UdpEchoServer(int port) throws SocketException {//网络编程一个常见的异常
        socket = new DatagramSocket(port); //端口号占用了则会new异常
    }


    // 使用这个方法启动服务器.
    public void start() throws IOException {//网络通信也是一种IO也会抛出IOException
        System.out.println("服务器启动!");//日志,证明启动了

        //一个服务器要给很多客户端提供服务
        //一个服务器不知道客户端啥时候来,所以反复的, 长期的执行针对客户端请求处理的逻辑(while)
        while (true) {
            // 反复的, 长期的执行针对客户端请求处理的逻辑.


            // 一个服务器, 运行过程中, 要做的事情, 主要是三个核心环节.


            // 1. 读取请求, 并解析
            //new一个用于读取请求的对象,创建此对象就要手动申请好内存空间(不能超过64KB),不像我们我们学过的集合类,内部有自己管理内存的能力
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            //DatagramPacket 是输出型参数,传入receive的对象是空,调用完receive后的空对象的内容就会被填充上
            //如果客户端的请求一直没来,recieve方法就会阻塞等待==>一直阻塞到真的有客户端发送东西来
            socket.receive(requestPacket);



            //把得到的数据报,转化成字符串,这样的转字符串的前提是, 后续客户端发的数据就是一个文本的字符串.getLength是数据报实际长度
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());



            // 2. 根据请求, 计算出响应
            //回显服务器这请求就是响应,因此假装调用下自己写的一个方法
            String response = process(request);



            // 3. 把响应写回给客户端
            // 此时需要告知网卡, 要发的内容是啥(获取response的内部字节数组和长度,长度要是字节长度), 要发给谁(客户端的IP和端口在requestPacket数据报里面).
            //DatagramPacket api都是按照字节来处理的,一定要传字节数组与字节长度(要相互对应)
            //DatagramPacket requestPacket 这个客户端传过来的对象 就包含向客户端通信的ip 和 port
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                    requestPacket.getSocketAddress());//requestPacket.getSocketAddress() 本质也是new了一个InetSocketAddress对象
            socket.send(responsePacket);//用send返回响应数据报



            // 记录日志, 方便观察程序执行效果.
            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 {
        UdpEchoServer server = new UdpEchoServer(9090);//端口号随便写的
        server.start();//启动服务器
    }
}

注意

  • 应为服务器能先获取到客户端的信息,相当于也获取了客户端的ip和端口,就不需要指定找谁了,通过穿过来的数据报就可以获取对方ip和端口号
  • 但是客户端是要找一个未知的服务器,因此要手动设置对方的端口号和ip
  • idea启动多个客户端,1.上面一排看到Help的右下边->点击向下的箭头,找到Edit Configurations-> 看到右边有个Modify options -> 勾选allow multiple instance.

6.0 补充

一些细节

  • UDP 客户端发向服务端为字节数组, 则 服务端也要用字节数组来接收,不然会出错

外网IP和内网IP

  • 我的udp服务器不能被别的电脑访问,是应为我当前电脑没有"外网ip",网络环境的现状,NAT机制是主流.NAT机制下就把我们的ip地址分为外网IP和内网IP(内网IP是不能直接访问的,但是外网IP是可以直接访问的,只要找到一个有外网IP的电脑就可以随时访问了),云服务器就有外网IP,我们以可以通过内网穿透来访问,但同样要依靠有外网IP的电脑

java在linux运行

  • 吧java文件打包成jar文件,传到linux上再运行就能部署了
  • 手动打包不需要学习,用maven打包更方便更简单
  • 手动打包->File–>ProjectStrcuture–> Artifacts–> 按+号new一个JAR–>from modules with …—> 选一个入口类(main Class),更改路径文件—>界面端点击build–>buildArtifact—>点击build即可–>在out就可以找到你打的包
  • **运行: **java -jar jar包名字,或者用shell语法写一个脚本
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

_Ap0stoL

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

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

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

打赏作者

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

抵扣说明:

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

余额充值