网络编程(一)---传输层协议和UDP数据报套接字编程

(一).传输层的协议

1.协议的概念

在传输层中,最重要的两个协议,一个是TCP协议 ,另一个是UDP协议

在应用层中,操作系统提供了一组api,用于传输层给应用层提供服务,这组api又叫做socket.api,由于TCP和UDP的差别非常大,在进行代码编写的时候也是不同的风格,所以对于socket.api来说,提供了两套接口。

2.协议的区别

对于TCP来说,是有连接,可靠传输,面向字节流,全双工

对于UDP来说,是无连接,不可靠传输,面向数据报,全双工

(1).有连接 vs 无连接

对于有无连接,事实上是一个抽象的概念,是虚拟的,逻辑上的连接并不是物理上的连接

对于TCP来说,TCP协议中就保存的对端的信息,即A和B进行通信,A和B先建立连接,然后A保存了B的信息,B保存了A的信息,这就是有连接

对于UDP来说,UDP协议本身不保存对方的信息,则就是无连接

(2).可靠传输 vs 不可靠传输

在进行网络传输的过程中,被传输的数据是非常容易造成**"丢包"**的,即如果传输的是"0101"但是在传输的过程中某些bit位就被修改了,这样乱了的数据就会被识别出来,然后将这样的数据给丢弃掉。例如光信号和电信号都是会受到外界的干扰的,或者说在传输过程中,某个时间点,路由器/交换机中实际需要转发的数据超过了设备能转发的上限,此时也会产生"丢包"现象

对于可靠的传输 ,不是保证数据包100%到达,而是尽可能的提高传输成功的效率,如果出现了丢包现象,能被感知到。虽然降低了丢包的概率,但是也是需要付出代价的,就是运行效率比较低

对于不可靠的传输,只是把数据发了,剩下的就不管了

(3).面向字节流 vs 面向数据报

面向字节流,即读取数据的时候,是以"字节"为单位的,支持任意长度,但是会发生"粘包问题",后面会具体介绍

面向数据报,即读写数据的时候,以一个数据报为单位,一次必须读一个UDP数据报,不能是半个,所以读取长度会进行限制,不存在"粘包"问题

(4).全双工 vs 半双工

对于全双工来说,一个通信链路,支持双向通信,既能读也能写

对于半双工来说,一个通信链路,只支持单向通信,要么能读要么能写

(二).UDP数据报套接字编程

1.概念

上面介绍过,操作系统提供了socket.api,我们可以通过socket.api进行网络编程。这是为什么?

这是因为, 对于计算机中的"文件"来说,还能够指代一些硬件设备,操作系统管理硬件设备也是抽象成文件,来进行统一管理的。

对于我们电脑中的网卡来说,就是抽象成了一个socket文件,在进行操作网卡的时候,流程和操作普通文件差不多,即 "打开 -> 读写 -> 关闭",对于直接操作网卡来说,是不好进行操控的,但是将网卡转换成socket文件,那么操作这个socket文件就相当于操作网卡了

2.具体的类

在使用UDP数据报套接字编程的时候,具体用到了两个类,一个类是DatagramSocket ,另一个类是DatagramPacket

3.构造方法

构造方法相当于打开文件

(1).DatagramSocket类

对于不带参数的构造方法,则会随机选择一个端口进行绑定

对于第三个构造方法,则会在创建socket的时候就会关联上一个端口号,使用端口号的目的就是为了区分主机上的不同的应用程序

(2).DatagramPacket类

DatagramPacket表示一个完整的UDP数据报,对于UDP数据报的载荷数据,就可以通过构造方法来指定

4.其他方法

receive()方法就是从套接字中接收数据报,如果没有接收到数据报,则会阻塞等待

send()放啊就是从套接字发送数据报,直接发送

可以看到,receive()和send()方法,里面的参数都是一个DatagramPacket引用对象

close()关闭此数据报套接字

5.模拟实现回显服务器和客户端

客户端给放服务器发送一个数据称为"请求",服务器返回一个数据称为"响应"

"回显服务器"就是请求是啥响应就是啥

(1).回显服务器

Ⅰ.创建对象
Ⅱ.写出构造方法

在构造方法中,指定一个端口号,让服务器来使用

Ⅲ.写出主循环

在主循环中,服务器的通常流程为①.接收请求并解析②.根据请求,计算响应③.把响应返回给客户端

上述代码就是主循环的过程

receive()这个方法,里面的参数requestPacket,其实是一个"输出型参数",receive()方法会将数据从网卡中读取出来,然后填充到参数中,所以我们在执行receive()方法之前,要先构造出一个空的DatagramPacket的对象,然后把这个对象传递到receive()方法的参数中

在进行"把响应返回给客户端"的代码中,我们首先要将响应之后的结果由String类型再转为二进制,然后计算出二进制数据的长度,注意:这里只能用response.getBytes().length,不能使用response.length(),因为二进制数据的长度和String类型的长度是不一样的。

同时,在进行返回客户端的操作的时候,由于UDP协议中并没有保存对方的信息,即目的ip和目的端口,所以在responPacket中要获取到目的ip和目的端口

那么我们应该如何获取?

我们可以想到,客户端在给服务器发送数据的时候,那么这个客户端就相当于源ip和源端口,所以说我们我们要找的目的ip和目的端口就可以从requestPacket中获取到,即服务器应该使用请求中的源ip和源端口作为响应的目的ip和目的端口

当我们点到getSocketAddress()内部中可以看到,它调用的是**InetSocketAddress()**方法来获取到的源ip和源端口

Ⅳ.写出主函数

可以看到,我这里指定的端口是9090

Ⅴ.完整版代码
java 复制代码
package network;

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

public class UDPEchoServer {
    //创建对象
    //socket对象代表网卡文件,读这个文件等于从网卡收数据,写这个文件等于让网卡发数据
    private DatagramSocket socket=null;

    public UDPEchoServer(int port) throws SocketException {
        //指定一个端口号,让服务器进行使用
        socket=new DatagramSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动\n");

        //对于服务器来说,客户端什么什么时候发送请求,服务器无法预测,因此服务器通常会有一个死循环,持续不断的读取客户端的请求数据
        while (true){  //循环一次,就相当于处理一次请求

            //1.接收请求并解析
            DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);
            //将获取到的二进制数据转换转换成字符串,方便以后的操作
            String request=new String(requestPacket.getData(),0,requestPacket.getLength());

            //2.根据请求计算响应
            String response=process(request);

            //3.把响应返回给客户端
            //                                               将相应的数据转为二进制       求出二进制的数据的长度          获取到源ip和源端口
            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(),requestPacket.getPort(),request,response);
        }

    }

    //由于写的是回显服务器,所以请求的是啥,响应就是啥
    private String process(String request) {
        return request;
    }


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

注意:

①.这里socket文件是不需要关闭的。因为这个socket伴随着整个UTP服务器自始至终,当服务器关闭,那么进程就结束了,此时PCB的文件描述符表中的所有资源就会被释放掉,也就不需要手动调用close()方法了。

②.当服务器启动的之后,客户端还没有发送请求,在客户端发送请求之前,服务器都在receive()方法中进行阻塞等待,只有当客户端请求发来了,receive()才会返回

(2).回显客户端

Ⅰ.创建对象
Ⅱ.写出构造方法

对于客户端来说,访问服务器时,要明确要访问的服务器的ip地址和端口号,所以在写构造方法的时候要明确服务器的ip地址和端口号

Ⅲ.写出主循环

在主循环中,客户端的通常流程为①.发送请求②.接收服务器的响应

上述就是主循环的代码

这里在构建DatagramPacket对象的时候,要将服务器的ip地址和端口号传进去,但是如果直接传String类型的serverIp的话,是会报错的

这是因为DatagramPacket的构造方法中并没有提供一个直接传递String类型的IP地址的构造方法,所以要用InetAddress.getByName()方法进行封装

Ⅳ.写出主函数

由于是在本机上进行客户端和服务器的传输,所以ip地址直接写本机地址即可,即127.0.0.1(环回ip),端口要和服务器用的端口一样,即9090

Ⅴ.完整版代码
java 复制代码
package network;

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

public class UdpEchoClient {
    private DatagramSocket socket=null;
    //记录服务器的ip地址和端口号
    private String serverIp;
    private int serverPoot;

    public UdpEchoClient(String serverIp, int serverPoot) throws SocketException {
        this.serverIp = serverIp;
        this.serverPoot = serverPoot;
        this.socket =new DatagramSocket();
    }

    public void start() throws IOException {
        Scanner scanner=new Scanner(System.in);
        while (true){
            System.out.println("请输入要发送的内容");
            //1.从控制台读取用户输入的内容
            if (!scanner.hasNext()){
                break;
            }
            String request=scanner.next();
            //2.把请求发送给服务器,需要构造DatagramPacket对象
            //在构造的过程中,不光需要载荷,还需要目的服务器的ip地址和端口号                                               这里需要对serverIp进行转换
            DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(serverIp),serverPoot);
            //3.发送数据报
            socket.send(requestPacket);
            //4.接收服务器的响应
            DatagramPacket responsePacket=new DatagramPacket(new byte[4096],4096);
            socket.receive(responsePacket);
            //5.从服务器读取的数据进行解析,打印出来
            String response=new String(responsePacket.getData(),0,responsePacket.getLength());
            System.out.println(response);
        }
    }

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

注意:

这里socket文件是不需要关闭的。因为这个socket伴随着整个UTP客户端自始至终,当客户端关闭,那么进程就结束了,此时PCB的文件描述符表中的所有资源就会被释放掉,也就不需要手动调用close()方法了。

(3).程序运行结果

6.模拟实现翻译服务器和客户端

对于不同服务器来说,不同的往往就是业务逻辑不同,上面写的回显服务器,客户端发来什么内容,那么服务器就返回什么内容。现在要写的这个翻译服务器,无非就是客户端发来汉语,服务器返回英语即可,所以只需要修改**process()**方法即可

那么我们对于这个翻译服务器来说,只需要继承回显服务器然后重写里面的process()方法即可,其他的基本没有改变

java 复制代码
package network;

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

public class UDPDictServer extends UDPEchoServer{

    public HashMap<String,String> hashMap=new HashMap<>();


    public UDPDictServer(int port) throws SocketException {
        super(port);
        //初始化字典
        hashMap.put("小猫","cat");
        hashMap.put("小狗","dog");
        hashMap.put("小鱼","fish");
        hashMap.put("小猴","monkey");
        hashMap.put("小虎","tiger");
    }

    public String process(String request){
        return hashMap.getOrDefault(request,"未找到该词条");
    }

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

同时注意,在重写process()方法的时候,要将父类的process()方法的访问修饰限定符修改为public,如果父类的方法是私有的,那么子类是无法进行重写的

相关推荐
zzipeng1 小时前
IMX6ULL CAN通讯应用学习
linux·运维·网络
乌托邦的逃亡者1 小时前
Ubuntu主机中,为一个网卡设置多个IP地址
服务器·网络·ubuntu
环流_1 小时前
NAT工作机制(中间人为请求和响应搭桥牵线)
网络·智能路由器
丝雨_xrc2 小时前
Claude Opus 4.7 新手快速上手指南
大数据·网络·人工智能
上海云盾-小余2 小时前
动态 IP 隐匿技术:手游服务器规避端口扫描与溯源攻击实战
服务器·网络协议·tcp/ip
Mr_sst2 小时前
文件上传并发控制:为什么选Redisson可过期信号量?(避坑指南)
网络·数据库·redis·分布式·安全架构
时空自由民.2 小时前
HTTP协议帧格式
网络·网络协议·http
汽车仪器仪表相关领域2 小时前
Kvaser Memorator R SemiPro:双通道CAN总线记录仪,汽车与工业测试的高性价比之选
大数据·网络·人工智能·功能测试·汽车·安全性测试
sdszoe49222 小时前
华为设备安全管理之路由器+ACL
网络·安全·华为·路由器+acl