网络编程一

一、初识网络

📃协议

        协议是我们接触互联网以来,听到的频率较高的一个术语,那它具体指的是什么呢?协议,是网络协议的简称,意思是在网络通信的过程中,所经过的网络设备必须要共同遵守一组约定,准则。举个例子来说,如果A君要去相亲,会提前和相亲对象说好,在某某咖啡厅,信物是志摩的诗夹带一束玫瑰。那么只要到达咖啡厅,见到拿着信物的人,那么就是A君的相亲对象了。

但是实际上在网络通信中,协议是非常复杂的,不同的环境就会有不同的协议。如果协议过于复杂需要对其进行拆分,将拆分好的协议在进行分类同时根据这些不同的类别进行分层。故就有要求:上层协议调用下层协议;下层协议给上层提供支持,不能跨层调用。

 📃基本概念

        在每一次的网络数据传输中,都会存在发送端和接收端。

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

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

        根据上面的描述同样也会存在请求和响应,服务端与客户端,客户端发来请求,服务端根据请求解析返回响应。

 📃真实的网络分层

        对于网络分层,当下最为广泛的分层是TCP/IP五层网络模型,即应用层、传输层、网络层、数据链路层、物理层;这里不做太多赘述,我们想要实现网络程序关注的是应用层和传输层。当我们编写一个网络程序时,需要上层协议调用下层协议,此时应用层要调用传输层,传输层给应用层提供一组API,统称为 Socket API。

  📃网络套接字Socket

       Socket 套接字是基于TCP/IP协议的网络通信的基本单元,是由系统提供用于网络通信的技术,所以认为基于Socket套接字的网络程序开发就是网络编程。

        Socket也提供了2种不同的API,一种是流套接字:传输层TCP协议,一种是数据报套接字:使用传输层UDP协议。

        我们本次先尝试学习数据报套接字,UDP协议。UDP就是User Datagram Protocol(用户数据协议),传输层协议,我们可以将数据报理解为一段语音,一条消息,通过网络介质将其传输,让请求方和接收方形成通信。

        

二、UDP

API

基于UDP协议的网络编程,存在2个API,一个是 DatagramSocket API ,另一个就是DatagramPacket API,逐一给大家介绍。

DatagramSocket是UDP Socket,是用于发送和接收UDP数据报。Datagram就是数据报,Socket则是说明这个对象是一个Socket对象,Socket对象是一个特殊的文件,是对应到网卡这个硬件设备上,和普通的文件(execl、ppt、world)不是一回事。我们如果想要进行网络通信,就必须要有Socket这样的对象,间接的去操作网卡;我们可以做如下区别。

📤 往Socket对象中写数据,相当于通过网卡发送消息。

📥从Socket对象中读数据,相当于通过网卡接收数据。

DatagramSocket API

 DatagramSocket 是UDP Socket,用于发送和接收UDP数据报,构造方法有2个。

    

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

🔨注意:2个Socket方法,一个绑定随机端口是给客户端使用,一个却需要指定端口给服务器使用,这其中是有一定的说法。

举个例子🍜

服务器相当于是一个面馆,不久前A君租了一个门面房,准备开一间陕西臊子面馆,于是印了传单,打了广告。传单上印好了地址,面馆位于光明路1688号LG层101室,门店的地址一定是提前就确认好的。当顾客来点餐,A君说,你先找个地方坐好,比方说顾客坐在C10d的位置,臊子面做好我给你端过去。

过了几天当这个顾客再来吃面的时候,面馆的位置肯定是没有变化的,还是在光明路1688号LG层101室,但是原先C10这个位置就不一定是空出来的,有可能有其他人坐在这个位置上。

所以,面馆作为服务器,位置(端口)一定是不能随意变的,需要指定,但是座位就不一样,只要是空的座位,你就可以坐下来点餐。

DatagramSocket 方法:

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

receive:接收数据报

send:发送数据报

close:关闭数据报,上面说到Socket也是一种文件,如果用完不关闭,会造成文件资源泄露的问题。


DatagramPacket API

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

DatagramPacket构造方法:

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

  

🔨注意:第一个版本不需要设置地址进去,通常用来接收消息。第二个版本需要设置地址进去,通常用来发送消息。

DatagramPackt方法:

方法签名方法说明

InetAddress

getAddress()

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

🔨注意:构造UDP发送的数据时,需要传入SocketAddress,这个对象可以使InetSocketAddress来创建

DatagramSocket和DatagramPacket的区别

socket是文件,是点餐的话筒,通过这个话筒来对话

packet是要传输的数据,是端上来的一碗面


 回显服务器(Echo Server)

  服务器

通过上述对Sockt API的了解,我们写一个简单服务端以及客户端的回显服务器,即客户端发送了一个请求,服务器返回一个一模一样的响应。类似一个复读机,我喊的是什么返回的就是什么。

一个服务器,主要做要做三个核心任务

  1. 读取请求并解析
  2. 根据请求计算响应(本次省略)
  3. 把响应返回客户端

     注意: 客户端和服务端是成对出现的👇

 构造Socket

我们可以将建立Socket理解为创建一个遥控器🕹️来操控网卡,毕竟是通过网卡来发送和接收数据。

public class UdpEchoServer{ //服务器
    private DatagramSocket socket = null; //只有通过Socket对象,才能间接的操作对象

}

 绑定端口

绑定了端口不一定能成功,如果我们需要绑定的端口已经被占用,此时绑定的端口就会出错,在统一时刻,同一个主机和端口只能被一个进程绑定(一碗面只能被点单的人吃掉)。

public UdpEchoServer (int port) throws SocketExeption{
    //构造Socket的同时需要绑定端口
    socket = new DatagramSocket(port);//port是端口的意思
}

启动服务器

在确保Socket和绑定端口成功后,开始启动服务器,这也是最核心的任务。

我们已经说过一个服务器主要任务有三件事,这是每一个服务器执行任务的逻辑,是一个死循环,所以需要一个while。因为服务器并不是只是用一次,一家面馆不可能只为一个顾客服务,一次服务结束了还有其他顾客,面馆里面也不可能只有一个顾客来吃饭。

public void start(){
    System.out.println("服务器启动");//提示
    while(true){
        //每次循环,要做三件事
        //1.读取并请求解析
        //2.根据请求计算响应(本次省略这个步骤)
        //3.把响应结果写回到客户端

    }
}

1.请求并解析

比方说,面馆来了顾客,顾客说我要一碗臊子面,我们收到了请求就开始解析,这个时候就要用到socket.receive()方法;这个方法的参数类型是输出类型参数。

输出型参数是什么意思呢,一般来说我们都是以参数来作为方法的"输入",用返回值作为方法的"输出"。这里的输入输出并不是键盘上的输入和输出。所谓输出型参数可以理解为去学校的食堂吃饭,但是我们并没用食堂的餐具,我们带着自己的空饭盒去打饭,阿姨会根据你想要吃哪些东西,把你的饭盒装满饭菜。receive就是类似于自己带着饭盒去打饭。

       


既然receive是输出型参数,饭盒也不会凭空出现,这个时候就需要我们去构造一个空的饭盒。

public void start(){
    System.out.println("服务器启动");//提示
    while(true){
        //每次循环,要做三件事
        //1.读取并请求解析
        //构造一个空的饭盒方便打饭
        DatagramPacket requestPacket = new DatagramPacket(new byte[1080],1080);
        socket.receive(requestPacket);
       
    }
}

空的饭盒构造好了,大小为1080,并没有实际的意义只是一个空饭盒。

我们构造出的 requestPacket 这里装的就是客户端的请求,这个饭哪里来的呢?从阿姨手上,也就是从网卡上来的。此时通过receive执行之后,就可以把读到的数据放到参数中了。

问:如果我们带着空饭盒去打饭,但是饭没好怎么办?

答:一旦服务器启动,调用start()方法,就会立即执行到receive这里,如果客户端此时没有发来请求,receive就会阻塞等待,阿姨只能看着你的空饭盒,直到后厨把饭做好才能打饭给你。


除了以上的请求我们还需要对requestPacket中的字节数组进行改造,如果客户端发来一条请求,老板来碗臊子面,多加香菜不要辣椒。这个数据就会以二进制的形式出现在requestPacket字节数组中,我们刚刚开辟的空间也就是空饭盒是以byte字节数组出现的,我们需要做的是将这个字节数组转换为String字符串,String里的内容就是老板来碗臊子面,多加香菜不要辣椒。

这个操作并不是强制要求,而是为了后续的代码简单处理。👇

public void start(){
    System.out.println("服务器启动");//提示
    while(true){
        //每次循环,要做三件事
        //1.读取并请求解析
        //构造一个空的饭盒方便打饭
        DatagramPacket requestPacket = new DatagramPacket(new byte[1080],1080);
        socket.receive(requestPacket);
        //将字节数组转换为String
        String request = new String(requsetPacket.getData(),0,requestPacket.getlength());
       
    }
}

2.根据请求计算响应时间

这里我们只是简单构造一个response作为响应,因为这是一个回显服务器,请求和响应的结果如出一辙。

public void start(){
    System.out.println("服务器启动");//提示
    while(true){
        //每次循环,要做三件事
        //1.读取并请求解析
        //构造一个空的饭盒方便打饭
        DatagramPacket requestPacket = new DatagramPacket(new byte[1080],1080);
        socket.receive(requestPacket);
        //将字节数组转换为String
        String request = new String(requsetPacket.getData(),0,requestPacket.getlength());
        
        //2.根据请求计算响应(此处省略这个步骤)
       String response = process(requset);
    }
}

public String process(String requsest){
    //这个process是根据请求计算响应
    //由于是一个回显服务器,写啥就是啥
    //之所以单独列出来,就是告知我们,这是服务器中的关键环节

    return request;
}

 3.把响应结果写回至客户端

客户端的请求发送了,内容也解析了,此时就需要把我们响应的结果写回至客户端。

我们细想一下,从网上买了一本书,这个时候需要发货了,作为卖家我得知道收件人的地址,这样才能把书寄出去,还得把这本书包装起来。我们根据刚才的请求response字符串,构造一个DatagramPacket,和请求不同的是,构造完了需要指定这个包是发送给谁的。

public void start(){
    System.out.println("服务器启动");//提示
    while(true){
        //每次循环,要做三件事
        //1.读取并请求解析
        //构造一个空的饭盒方便打饭
        DatagramPacket requestPacket = new DatagramPacket(new byte[1080],1080);
        socket.receive(requestPacket);
        //将字节数组转换为String
        String request = new String(requsetPacket.getData(),0,requestPacket.getlength());
        
        //2.根据请求计算响应(此处省略这个步骤)
       String response = process(requset);

        //3.把响应结果写回到客户端
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
                                             response.getBytes.length,
                                             requestPacket.getSocketAddress());
            socket.send(responsePacket);
    }
}

public String process(String requsest){
    //这个process是根据请求计算响应
    //由于是一个回显服务器,写啥就是啥
    //之所以单独列出来,就是告知我们,这是服务器中的关键环节

    return request;
}

还记得我们是如何构造那个空饭盒的吗,字节数,字节长度。

requestPacket是通过客户端发送过来的,所以getSocketAddress()这个方法就可以得到客户端的IP和端口号。👇

getSocketAddress同时包含了IP和端口号,此时我们就需要根据这个请求把结果发送回去。

打包裹的过程

发包裹的过程

打印出日志

System.out.printf("[%s:%d] req: %s, resp: %s\n",
                    requestPacket.getSocketAddress().toString(),
                    requestPacket.getPort(),request,response);

4.设置端口

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

    }

客户端

写好了服务器,此时我们就需要写客户端的代码

此时也是需要创建一个Socket,发送和接收都需要Socket

public class UdpEchoClient {
    private DatagramSocket socket =null;
    private String serverIP;//IP地址
    private int serverPort;//端口号
}

除了Socket还需要有地址和端口号,客人来吃饭,总得知道你的店铺在哪里。


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

对于客户端而言是不需要关联端口的,但这不代表客户端没有端口,而是系统自动分配了空闲的端口,客人周一来吃饭坐在C10的位置,周二再来的时候不一定C10就是空出的状态。


启动客户端需要做的操作如下

1.从控制台读取一个字符串

2.把字符串构造成一个UDP Packet,并进行发送

3.客户端尝试读取服务器返回的响应

4.把响应数据转换成 String 显示出来

读取请求

 public void start() throws IOException{
      //通过这个客户端可以多次和服务器进行交互
       Scanner scanner = new Scanner(System.in);

   }

Scanner开始读取了,还是和服务器端一样,启动也是在while循环中。

public void start() throws IOException{
        //通过这个客户端可以多次和服务器进行交互
        Scanner scanner = new Scanner(System.in);
        while(true){
            //1.从控制台,读取一个字符串过来
            // 先打印一个提示符,提示用户要输入内容
            System.out.println("->");
            String request = scanner.next();//读取字符串

}

构造UDP Packet

我们在服务端发送了一个请求,这个请求最终被构造成了一个DatagramPacket发送,那么作为客户端的响应,我们也需要将已经读取的字符串构造成一个UDP Packet进行发送。

对比服务器和客户端两段构造UDP的代码看来,不难发现,基本上是一样的,只不过服务器是接收响应,而客户端是构造请求并回复服务器,至于细节都是通过String来读取,这样有利于我们的操作;客户端的构造一方面是需要String中的getBytes数组,另一方面则是需要指定服务器的IP和端口,此处不再是通过InetAddress直接构造了,而是分开构造,服务器在返回响应的时候,是直接从packet里面取出的InetAddress。

  public void start() throws IOException{
        //通过这个客户端可以多次和服务器进行交互
        Scanner scanner = new Scanner(System.in);
        while(true){
            //1.从控制台,读取一个字符串过来
            // 先打印一个提示符,提示用户要输入内容
            System.out.println("->");
            String request = scanner.next();//读取字符串

            //2.把字符串构造成 UDP packet,并进行发送

            DatagramPacket requestPacket = new DatagramPacket(
                    request.getBytes(),
                    request.getBytes().length,
                    InetAddress.getByName(serverIP),serverPort);
            //取到字节数组,取到数组的长度,把服务器的IP地址构造进去
            socket.send(requestPacket);
}

客户端读取响应

构造完了UDP Packet数据报以后,就需要读取来自服务器的响应了,如何读取呢?还是用到了空饭盒打饭的原理,我们再次构造一个空饭盒用于接收来自服务器的响应。

DatagramPacket responsePacket = new DatagramPacket(new byte[1080],1080);
//构造一个空的字节数组负责去接收
Socket.receive(responsePacket);
//把空的数据传送到receive中,负责填充

 响应数据转换

OK,那么这个时候已经有来自服务器的响应了,但是这个数据是以二进制的形式展现出来的,所以还是需要以String的方式转换一下

String respponse = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.printf("req: %s, req: %s\n",request,response);

设置端口

因为我们是一个回显服务器,是在同一台主机上发送请求和返回响应的,所以我们的端口需要和服务器端保持一致

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

启动顺序

接下来我们将服务器和客户端放在一起看下启动的效果

📃记住:一定是服务器先启动然后才是客户端启动,面馆是一定先存在于店铺之前

//启动服务器的主逻辑
    public void start() throws IOException {
        System.out.println("服务器启动!!");
        while (true){
           //1.读取请求并解析
            //构造一个空的饭盒
              DatagramPacket requestPacket = new DatagramPacket(new byte[4090],4090);
            //食堂大妈给饭盒打饭,饭从网卡里面来
              socket.receive(requestPacket);
              //为了方便处理请求,把数据报转成String
              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());
            //requestPacket 是从客户端这里收来的,所以getSocketAddress就会得到客户端的IP和端口
               //注意:getSocketAddress同时包含了IP和端口号
            socket.send(responsePacket);//发送响应的数据报


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

1.服务器先启动,直接来到receive这里,并进行阻塞等待

2.客户端开始运行,来客人了,从控制台读取数据,并发送send,此时服务器和客户端都会往下执行代码

3.客户端发送之后,继续往下走,走到receive这里读取响应;

  服务器就会从receive这里返回,读到请求数据(由客户端发来的)再往下走到send,并打印日志

4.进入下一轮的循环,服务器再次阻塞在receive,刚才的面已经吃完了,会有新的顾客来点餐;等待下一次请求客户端这边真正收到服务器send回来的数据后就会接触阻塞,有人点单了,执行下面的打印日志操作;如下图,会再次阻塞在receive这里,直到收到有从通过网卡发来的数据。

5.客户端会进入下一轮的循环,阻塞在等待用户输入数据这里->scanner.next。

运行结果

先启动服务器,提示服务器启动

再启动客户端,输入hello\

服务器返回响应,输出hello\

回显服务器输入是什么,返回的就是什么。

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值