奇思妙想录 所谓天才,就是努力的力量。——德怀特
歌曲封面 未知作品

萌ICP备20248808号

津ICP备2023004371号-2

网站已运行 2 年 5 天 21 小时 30 分

Powered by Typecho & Sunny

0 online

Title

网络编程

IhaveBB

·

Article
⚠️ 本文最后更新于2023年11月14日,已经过了248天没有更新,若内容或图片失效,请留言反馈
AI摘要:本文介绍了网络编程中的UDP和TCP协议,包括它们的API、代码示例和网络原理。UDP是一种无连接、不可靠、面向数据报的传输层协议,适用于不需要建立连接的场景。TCP则是一种面向字节流、可靠的传输层协议,适用于需要稳定传输的场景。文章通过echo回显服务器和翻译服务器的示例代码,展示了UDP和TCP在实际应用中的使用。同时,还介绍了UDP报文结构和特点,以及基于UDP的应用层协议。

Powered by AISummary.

网络编程

一.UDP

1.1 api

1.1.1 DatagramSocket

DatagramSocket是一个Scocket对象。那么Socket又是什么呢?

我们的操作系统中,往往使用文件这样的概念来管理一些软硬件资源。我们这里的Socket文件,就是用来管理网卡的一种文件。

Java中的Socket对象,就对应了系统里的Socket文件,要进行网络通信,必须得先有Socket对象。

♾️ java 代码:
DatagramSocket()
DatagramSocket(int port)

DatagramSocket有带端口和不带端口的构造方法,那么我们应该如何选择呢?

当在客户端使用时,我们往往让系统来自动分配,而在服务端时,我们往往手动指定。

1.1.2 DatagramPacket

DatagramPacket表示了一个UDP数据报,代表了系统中设定的UDP数据报的二进制结构。

UDP是面向数据报的,每次进行传输,都要以UDP数据报为基本单位。

下面是DatagramPacket的几个构造方法,DatagramPacket作为UDP数据报,必然要承载一部分的数据,这就需要我们通过手动指定的byte[]作为数据存储的空间

♾️ java 代码:
public DatagramPacket(byte[] buf, int length,InetAddress address, int port)
public DatagramPacket(byte[] buf, int length)    
    ....

1.2 代码示例

1.2.1echo回显服务器

♾️ java 代码:
package network;

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

public class UdpEchoServer {
    //创建一个DatagramScocket对象
    //这是后续操作网卡的基础
    private DatagramSocket socket = null;

    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
        /*
        我们在服务器和客户端中均需要创建一个Socket对象
        但是服务器的socket一般需要显示的指定一个端口号
        而客户端的socket一般不能显示指定(不显示指定时候系统会自动分配一个随机的端口)
         */
    }

    //通过这个方法开启动服务器
    public void start() throws IOException {
        System.out.println("服务器启动");
        while(true){
            //我们在使用socket.receive来接受数据时,需要先创建一个数据报(DatagramPacket)用来接受从网卡读取到的数据,
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            //1.读取请求并解析
            socket.receive(requestPacket);
            //当前完成recive之后,数据以二进制的形式存储到DatagramPacket中
            //要想能够把数据显示出来,还需要把二进制转成字符串
            String request = new String(requestPacket.getData(),0, requestPacket.getLength());
            //2.由于我们此处是回显服务器,所以请求=响应。
            String reponse = process(request);
            //3.把响应写回客户端
            //  搞一个响应对象,DatagramPacket
            //  往DatagramPacket中填入刚才的数据,在通过sent返回
            //传入数据报,数据报长度,和请求中的地址
            DatagramPacket reponsePacket = new DatagramPacket(reponse.getBytes(),reponse.getBytes().length,requestPacket.getSocketAddress());
            //注意这个长度不可以更改成reponse.length,因为如果这个字符串的内容都是英文字符,此时字节和字符个数是一样的,但是如果包含中文就不一样了。
            socket.send(reponsePacket);
            //4.打印日志
            System.out.printf("[%s:%d] req=%s res=%s\n",reponsePacket.getAddress().toString(),reponsePacket.getPort(),request,reponse);

        }
    }

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

    public static void main(String[] args) throws IOException {

        UdpEchoServer udpEchoServer = new UdpEchoServer(9090);
        udpEchoServer.start();
    }
}
package network;

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

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String severIp = "";
    private int severPort = 0;
    public UdpEchoClient(String severIp,int severPort) throws SocketException {
        //创建这个对象不能手动指定端口
        socket = new DatagramSocket();
        //由于UDP自身不回持有对端的信息,就需要在应用程序中把对端的情况记录下来。
        this.severIp = severIp;
        this.severPort = severPort;
    }

    public void start() throws IOException {
        System.out.println("客户端启动");
        Scanner sc = new Scanner(System.in);
        while(true){
            //1.从控制台读取一个数据,作为请求
            System.out.print("->");
            String request = sc.next();
            //2.把请求内容构造成一个DatagramPacket对象,发送给服务器
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName((severIp)), severPort);
            socket.send(requestPacket);
            //3.尝试读取服务器返回的响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(responsePacket);
            //4.把响应转换成字符串,并打印输出
            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();
    }
}

1.2.2 翻译服务器

♾️ java 代码:
package network;

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("dog", "小狗");
        dict.put("cat", "小猫");
    }

    @Override
    public String process(String request) {
        return dict.get(request);
    }

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

1.2.3 总结

1.服务器先启动,服务器启动之后,就会进入循环,找到receive并阻塞(当客户端还未传入数据时)

2.客户端启动,也会先进入while循环,执行sc.next,并且也在这里阻塞

​ 当用户在控制台输入字符串之后,next就会返回,从而构造请求数据并发送给服务器。

3.客户端发送出数据之后

​ 服务器:就会从receive中返回,进一步的执行解析请求为字符串,执行process操作,执行send操作

​ 客户端:继续往下执行,执行到receive等待服务器的响应、

4.客户端收到从服务器返回的数据后,就会从receive中返回

​ 执行到这里的打印操作,接着显示出响应内容

5.服务器完成一次循环之后,又会执行到reveive

​ 客户端完成一次循环之后,又会执行到sc.next

​ 双双进入阻塞等待

2.TCP

2.1 API

TCP的socketapi和UDP的socket api差异很大,接下来让我们往下看

2.1.1 ServerSocket

给服务器使用的类,用来绑定端口号

2.1.2 Socket

既会给服务器用,又会给客户端使用

2.1.3 对比

  1. TPC是有连接的 UDP是无连接
  2. TCP是可靠传输 UDP是不可靠传输
  3. TCP是面向字节流 UDP是面向数据报
  4. TCP和 UDP都是全双工

连接:通信双方是否会记录保存对端的信息。

UDP:每次发送数据都需要手动在send方法中指定

TCP:连接如何建立,不需要代码干预,由系统内核自动完成

对应用程序来说:

  • 客户端主要是发起“建立连接”的动作
  • 服务器主要是把建立好的连接从内核中拿到应用程序里

如果有客户端和服务器要建立连接,这时服务器的应用程序是没有任何感知的,内核直接就完成了建立连接的流程(三次握手),完成流程之后,就会在内核的队列中(每一个ServerSocket都有这样的一个队列)排队。

应用程序要想和客户端进行通信,就需要通过一个accept方法,把内核中已经建立好的连接对象,拿到应用程序中

2.2代码示例

2.2.1代码

♾️ java 代码:
package network;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TcpEchoServer {
    private ServerSocket severSocket = null;

    public TcpEchoServer(int port) throws IOException {
        severSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动");
        ExecutorService service = Executors.newCachedThreadPool();
        while(true){
            //通过accetp方法,把内核中已经建立 好的链接拿到应用中
            //建立连接的细节流程是内核自动完成的,应用程序只需要捡现成的即可
            Socket clientSocket = severSocket.accept();
            service.submit(new Runnable(){
                @Override
                public void run(){
                    processConnection(clientSocket);
                }
            });
        }
    }

    //通过这个方法来处理当前的连接
    public void processConnection(Socket clientSocket){
        //进入方法,打印出一个日志,表示当前有客户端连接上了
        System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()) {
            // 使用 try ( ) 方式, 避免后续用完了流对象, 忘记关闭.
            // 由于客户端发来的数据, 可能是 "多条数据", 针对多条数据, 就循环的处理.
            while(true){
                Scanner sc = new Scanner(inputStream);
                if(!sc.hasNext()){
                    // 连接断开了. 此时循环就应该结束
                    System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
                    break;
                }
                // 1. 读取请求并解析. 此处就以 next 来作为读取请求的方式. next 的规则是, 读到 "空白符" 就返回.
                String request = sc.next();
                // 2. 根据请求, 计算响应.
                String response = processs(request);
                // 3. 把响应写回到客户端.
                //    可以把 String 转成字节数组, 写入到 OutputStream
                //    也可以使用 PrintWriter 把 OutputStream 包裹一下, 来写入字符串.
                PrintWriter printWriter = new PrintWriter(outputStream);
                //    此处的 println 不是打印到控制台了, 而是写入到 outputStream 对应的流对象中, 也就是写入到 clientSocket 里面.
                //    自然这个数据也就通过网络发送出去了. (发给当前这个连接的另外一端)
                //    此处使用 println 带有 \n 也是为了后续 客户端这边 可以使用 scanner.next 来读取数据.
                printWriter.println(response);
                //    此处还要记得有个操作, 刷新缓冲区. 如果没有刷新操作, 可能数据仍然是在内存中, 没有被写入网卡.
                printWriter.flush();
                // 4. 打印一下这次请求交互过程的内容
                System.out.printf("[%s:%d] req=%s, resp=%s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally{
            try {
                // 在这个地方, 进行 clientSocket 的关闭.
                // processConnection 就是在处理一个连接. 这个方法执行完毕, 这个连接也就处理完了.
                clientSocket.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

    }

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

    public static void main(String[] args) throws IOException {
        TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
        tcpEchoServer.start();
    }
}
♾️ java 代码:
package network;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

public class TcpEchoClient {
    private Socket socket = null;

    public TcpEchoClient(String serverIp,int ServerPort) throws IOException {
        // 需要在创建 Socket 的同时, 和服务器 "建立连接", 此时就得告诉 Socket 服务器在哪里~~
        // 具体建立连接的细节, 不需要咱们代码手动干预. 是内核自动负责的.
        // 当我们 new 这个对象的时候, 操作系统内核, 就开始进行 三次握手 具体细节, 完成建立连接的过程了.
        socket = new Socket(serverIp,ServerPort);
    }
    public void start(){
        Scanner sc = new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            PrintWriter writer = new PrintWriter(outputStream);
            Scanner scannerNetWork = new Scanner(inputStream);
            while(true){
                //1.从控制台读取用户输入的内容
                System.out.println("->");
                String request = sc.next();
                // 2. 把字符串作为请求, 发送给服务器
                //    这里使用 println, 是为了让请求后面带上换行.
                //    也就是和服务器读取请求, scanner.next 呼应
                writer.println(request);
                writer.flush();
                //3.读取服务器返回的相应
                String response = scannerNetWork.next();
                //4.输出内容
                System.out.println(response);
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

2.2.2 注意事项

clientSocket需要进行close操作
♾️ java 代码:
Socket clientSocket

在UDP中的DatagramSocket和ServerSocket都没有写close,那为什么没问题呢?

因为UDP中的DatagramSocket和ServerSocket的周期是贯穿着整个程序的,只有这么一个对象,所以并不会频繁的创建,因此不会造成内存泄漏问题。

但是TCP中,ClientSocket是在一个循环中,每当有新的客户端来建立连接就会创建出一个新的ClienbtSocket

虽然我们已经在下面进行了trycatch操作,但这里关闭的知识clinetSocker上自带的流对象,并没有关闭socket本身

image-20231022121135160

所以,我们需要在代码中,通过finally加上close,来确保socket能被正确的关闭

image-20231022121910772
使用线程池来建立多端连接

为什么我们需要线程池来处理多端连接问题呢?

image-20231022130700601

如图所示,图上的代码并没有使用线程池,接下来让我们分析一下问题

第一个客户端连接之后,accept就返回了,得到了一个clientSocket,进入了processConnection

接下来又进入了一个while循环,在这个循环中,需要反复处理客户端发来的请求数据,如果客户端这会还没有发送请求,服务器代码就会阻塞在sc.hasNext这里

此时此刻,第二个客户端来连接了,此时是可以连接成功的(因为连接由内核负责)建立连接之后,连接对象就会在内核的队列里等待,直到代码通过accept把连接取出来。

但是当前的代码是无法第一时间执行到第二次的accept。第一个客户端会使服务器处于processConnection内部,直到第一个客户端退出,processConnection才能结束,才能执行后续的操作。

出现这个问题的关键点在于两重循环在一个线程里,进入第二重循环的时候,无法继续执行第一个循环,UDP服务器中只有一个循环,故不会出现这个问题,所以我们应使用多线程来解决。

image-20231022131028252

网络原理

一.UDP

1.1 UDP报文结构

UDP是传输层协议之一,其主要特点是无连接,不可靠传输,面向数据报,全双工

image-20231026205157171

UDP报文主体分为两个部分:UDP报头(占8个字节)+UDP数据/UDP载荷

UPD报头:源端口号+目的端口号+数据报长度+校验和

  • 源端口号和目的端口号

    • 源端口号和目的端口号均占用16个比特位,即为2个字节
  • UDP长度

    • 总共16位,占两个字节
    • UDP报文长度=UDP报头(首部)+UDP载荷
    • 2个字节能表示的数据范围是0~65535,也就是能够表示的报文长度是65536字节(Byte),转换成KB,65536/1024 = 64 KB 这就是一个UDP报文所能表示的最大长度.
  • 校验和

    • 数据在传输的时候,本质上是0/1bit流,通过光信号或者电信号来表示,如果在传输的时候收到干扰,就可能会出现比特翻转现象.这个时候就需要校验和校验数据是否出错.

1.2UDP报文特点

1.无连接

知道对端的 IP 和端口号就直接进行传输,不需要建立连接;

2.不可靠

没有任何安全机制,发送端发送数据报以后,如果因为某些问题无法发送到对端, UDP 协议层也不会给应用层返回任何错误信息;

3.面向数据报

应用层无论交给 UDP 多大的报文,UDP原样发送,不会拆分或者合并;

4.缓冲区

UDP 只有接收缓冲区,没有发送缓冲区

5.大小受限

UDP 协议首部中有一个 16 位的最大长度。也就是说一个 UDP 能传输的数据最大长度是 64K (包含 UDP首部)。

1.3 基于UDP的应用层协议

  • NFS:网络文件系统
  • TFTP:简单文件传输协议
  • DHCP:动态主机配置协议
  • BOOTP:启动协议(用于无盘设备启动)
  • DNS:域名解析协议
现在已有 0 条评论,0 人点赞
Comment
发表
搜 索 消 息 足 迹
你还不曾留言过..
你还不曾留下足迹..
博主