计算机网络——基于UDP与TCP网络编程

时间:2022-10-23 07:53:33

目录

一、什么是UDP与TCP

二、什么是Socket

三、UDP网络编程简单实现

1、DatagramSocket API

2、DatagramPacket API

3、基本使用方法:

我们以一个简易的翻译器的案例,来实现简单的UDP编程:

服务器端代码:

 部分代码的说明:

 客户端代码:

部分代码说明: 

测试结果:

四、TCP网络编程简单实现

ServerSocket API 

 Socket API

接下来以一个简单的回显服务器来说明TCP编程。

1、建立服务器端

部分代码说明:

2.建立客户端

测试结果:


一、什么是UDP与TCP

TCP与UDP是计算机网络中五层模型的运输层协议。

UDP协议:(User Datagram Protocol)是一种数据报文协议,它是无连接协议不保证可 靠传输。 因为UDP协议在通信前不需要建立连接,因此它的传输效率比TCP高

TCP 协议:是传输控制协议,它是面向连接的协议,支持可靠传输和双向通信。它在传输数据之前需要先建立连接,建立连接后才能传输数据,传输完后还需要断开连接。TCP协议之所以能保证数据的可靠传输,是通过接收确认、超时重传这些机制实现的。并且,TCP协议允许双向通信,即通信双方可以同时发送和接收数据。

对比来看两个协议的特点:

TCP:有连接;可靠传输;面向字节流;全双工

UDP:无连接;不可靠传输;面向数据报;全双工

有连接:比如打电话,得先接通,才能相互交互数据

无连接:想发微信,不需要接通,直接就能发数据

可靠传输:传输过程中,发送方知道接受方有没有收到数据

不可靠传输:传输过程中,发送方不知道接收方有没有收到数据

面向字节流:依字节为单位进行传输(非常类似于文件操作中的字节流)

面向数据报:以数据报为单位进行传输(一个数据报都会明确大小)一次发送/接受必须是一个完整的数据报,不能是半个或者一个半

全双工:一条链路,双向通信(双行道)

半双工:一条链路,单向通信(单行道)



二、什么是Socket

        在我们进行网络编程时,都会接触到一个名叫Scoket的概念,应用程序通过Scoket来建立远程连接,Socket通过内部封装好的协议把数据传输到网络。网络编程套接字,是操作系统给应用程序提供的一组API(叫做socket API)

        为什么需要Socket 进行网络通信?因为仅仅通过IP地址进行通信是不够的,同一台计算机同一时间会运行多个网络应用程序,例如浏览器、QQ、邮件客户端等。当操作系统接收到一个数据包的时候,如果 只有IP地址,它没法判断应该发给哪个应用程序,所以,操作系统抽象出Socket 接口,每个应用程序需 要各自对应到不同的Socket,数据包才能根据 Socket正确地发到对应的应用程序。socket可以视为应用层和传输层之间的通信桥梁。

        使用Socket进行网络编程时,本质上就是两个进程之间的网络通信。其中一个进程必须充当服务器端,它会主动监听某个指定的端口,另一个进程必须充当客户端,它必须主动连接服务器的IP地址和指定端口,如果连接成功,服务器端和客户端就成功地建立了一个TCP连接,双方后续就可以随时发送和接收,因此,当Socket 连接成功地在服务器端和客户端之间建立后:对服务器端来说,它的Socket是指定的IP地址和指定的端口号。



三、UDP网络编程简单实现

        UDP和TCP相比,就简单的一点,因为UDP不需要建立连接,也就没有区分哪一个是客户端哪一个是服务器端,是依靠数据包来进行实现的,数据包也是收一个发一个,不存在使用流的概念。

UDP也是需要使用Socket来监听端口的的,不过它使用的是java提供的DatagramSocket。

1、DatagramSocket API

DatagramSocket API是UDP Socket,用于发送和接受UDP数据报

计算机网络——基于UDP与TCP网络编程

2、DatagramPacket API

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

 计算机网络——基于UDP与TCP网络编程

注:构造UDP发送的数据时,需要传入SocketAddress,该对象可以使用InetSocketAddress来创建。 

计算机网络——基于UDP与TCP网络编程

3、基本使用方法:

服务端:

  • 1.创建一个DatagramSocket对象,创建的同时关联一个端口号
  • 2.读取请求,并解析
  • 3.根据请求计算响应
  • 4.把响应写回到客户端
  • 5.打印日志

客户端:

  • 1.创建一个DatagramSocket对象,创建的同时指定服务器的ip和端口号
  • 2.读取输入的数据
  • 3.构造请求并发送给服务器
  • 4.从服务器读取响应
  • 5.把数据显示给用户

我们以一个简易的翻译器的案例,来实现简单的UDP编程:

服务器端代码:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;


public class UdpEchoServer {
    //创一个DatagramSocket对象中
    private DatagramSocket socket = null;

    //翻译就是从key——>value的过程
    private Map<String,String> dict = new HashMap<>();

    //构造方法:
    //参数的端口表示我们的服务器要绑定的端口
    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);

        //这里利用HashMap存放大量的key,value值
        dict.put("玫瑰","rose");
        dict.put("许愿","Wishing");
        dict.put("不期而遇",
                "unexpected encounters ");
    }

    //启动服务器
    public void start() throws IOException {
        System.out.println("服务器启动!");
        //UDP不需要建立连接,直接接收从客户端传来的数据
        //死循环:不断接收客户端的连接
        while(true) {
            //每循环一次,处理一次请求

            //1、读取请求并解析
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            //接收
            socket.receive(requestPacket);
            //把这个datagramPacket对象转成字符串,方便打印
            String request = new String(requestPacket.getData(),0, requestPacket.getLength());

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

            //3、把响应写回到客户端
            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 Data) {
        return dict.getOrDefault(Data,"词典中未找到");
    }

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

 部分代码的说明:

在构造方法中,port参数的是什么意思:

        port参数,就是表示该服务器要绑定的端口,目的在于能够让客户端明确是访问主机上的哪个进程,通过端口确定一个进程,就需要在这个进程启动的时候绑定一个端口,并且一个端口只能被一个进程绑定(一般)

为什么使用死循环:

        服务器是不知道客户端啥时候发送请求,所以需要时刻准备着,接收

receive() 方法说明:

        这个方法的参数,是个输出型参数,调用receive的时候,就需要构造一个空的 DatagramPacket对象,然后把对象交给receive,在receive里面负责把从网卡读到的数据,给填充到这个对象里面

注:构造对象时,里面的new byte[4096],这个是自己调整大小,不要太小就好了                

两次构造对象的区别:

计算机网络——基于UDP与TCP网络编程

requestPacket.getSocketAddress()的作用:

计算机网络——基于UDP与TCP网络编程

记录客户端的IP和端口号

例如:

        我买了一个快递,现在想要退货,就需要把这个包裹发回给商家,商家的收货地址和收件人都是在我收到的包裹上写着的。 

 日志打印:

计算机网络——基于UDP与TCP网络编程


 客户端代码:

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


public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIP;
    private int serverPort;

    //构造方法:
    public UdpEchoClient(String serverIP,int serverPort) throws SocketException {
        //自动让系统指定一个空闲的窗口
        socket = new DatagramSocket();

        this.serverIP = serverIP;
        this.serverPort = serverPort;
    }

    //客户端
    public void start() throws IOException {
        Scanner sc = new Scanner(System.in);
        while(true) {
            //1、从控制台读取用户输入的内容
            System.out.println("->");
            String request = sc.next();

            //2、构造一个UDP请求,发送给服务器
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName(this.serverIP),this.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());

            //4、把服务器的响应显示到控制台上
            System.out.println(response);
        }

    }

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

部分代码说明: 

构造方法的参数:

public UdpEchoClient(String serverIP,int serverPort) {}

 前者是服务器IP,后者是服务器端口

疑问?为什么服务器那边,只是端口号,因为服务器一般情况下,IP就是本机IP

为什么让系统自动指定窗口? 

socket = new DatagramSocket();

         一般情况下,都是系统自动指定的,如果是手动指定,刚好所指定的窗口正在被别人使用,就会引来不必要的麻烦

举例:

        我们去餐厅吃饭,每次去我们都喜欢坐在一个拐角处(指定窗口),突然有一天,这里被别人坐了,我们总不能去赶走人家吧,最好还是做个空位做下就好啦,所以通常都是有系统自动指定一个空闲的窗口

关于DatagramPacket构造:

计算机网络——基于UDP与TCP网络编程

 

整体的运行流程:

计算机网络——基于UDP与TCP网络编程

测试结果:

计算机网络——基于UDP与TCP网络编程



四、TCP网络编程简单实现


        TCP的编程实现客户端与服务器端交互,是通过建立连接,从而利用“流”来进行数据交换的,即使用字节输入/输出流,把要发送的数据,存储到字节数组,通过“流”来实现发送与接收。

ServerSocket API 

计算机网络——基于UDP与TCP网络编程

 Socket API

计算机网络——基于UDP与TCP网络编程

接下来以一个简单的回显服务器来说明TCP编程。

1、建立服务器端

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 {
    //代码中会涉及到多个socket对象,使用不同的名字来区分
    private ServerSocket listenSocket = null;

    public TcpEchoServer(int port) throws IOException {
        listenSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        ExecutorService service = Executors.newCachedThreadPool();
        while(true) {
            //1、先调用accept来接受客户端的连接
            //如果当前没有客户端来建立连接,accept就会阻塞
            Socket clientSocket = listenSocket.accept();
            //2、再处理这个连接,这里应该要使用多线程,每个客户端上来都分配一个新的线程负责处理


            service.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }
    }

    private  void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d 客户端上线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort() );
        //接下来处理客户端的请求
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()) {
            while(true) {
                //1、读取请求并解析
                Scanner sc = new Scanner(inputStream);
                if(!sc.hasNext()) {
                    //读完了,连接断开了
                    System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress().toString());
                    break;
                }
                String request = sc.next();
                //2、根据请求计算响应
                String response = process(request);
                //3、把响应写回给客户端
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(request);
                //刷新缓冲区确保数据确实是通过网卡发送出去了
                printWriter.flush();

                System.out.printf("%s:%d req:%s;resp:%s\n",clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(),request,response);

            }
        }catch(IOException e) {
            e.printStackTrace();
        } finally {
            //这个关闭socket
            clientSocket.close();

        }
    }

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

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

部分代码说明:

为什么使用线程池?

        为了实现一个服务器能够并发响应多个客户端的请求,这里引入多线程的方法

        1,因为listen()监听函数过后,服务器的ip与端口就会暴露在网络中,网络中连接的各个客户端就可以连接该服务器,而所有的连接请求都会存储在监听文件描述符对应的读缓冲区中,每执行一次accept,就会从该监听文件描述符对应的读缓冲区中读取一个连接,因此,如果是多线程服务器,应该在主线程中将accept函数包含在一个while(true)循环中,让主线程不断从该缓冲区中接收连接。
        2,当accept函数执行完以后,就要有对应的子线程处理accept函数返回的客户端,因此,在while循环内部,每当执行完accept成功以后,就创建一个子线程,让该线程去处理该客户端。子线程内部的流程就是与客户端互相交流的一些代码。

为什么关闭socket

        socket也是一个文件,一个进程能够同时打开的文件个数有上限(PCB文件描述符表是有限的),listenSocket对象在TCP服务器程序中,只有一个唯一的对象,一般不会把文件描述符表占满(随着进程的结束,自动释放)。而clientSocket是在死循环里面的,每次来一个客户端,建立连接,都要分配一个,这个对象就会被反复创建按销毁实例,每创建一个,都要销毁一个文件描述符,因此需要把不再使用的clientSocket及时释放掉


2.建立客户端
 

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 {
    //客户端需要使用这个socket对象来建立连接
    private Socket socket = null;

    public TcpEchoClient(String serverIP,int serverPort) throws IOException {
        //和服务器建立连接,就需要知道服务器在哪儿了
        //这里和UDP客户端差别比较大
        socket = new Socket(serverIP,serverPort);
    }
    public void start() {
        Scanner sc = new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            while (true) {
                //1、从控制台读取数据,构成一个请求
                System.out.println("->");
                String request = sc.next();
                //2、发送请求给服务器
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(request);
                //这个flush不要忘记,否则可能导致请求没有真发出去
                printWriter.flush();
                //3、从服务器读取响应
                Scanner respScanner = new Scanner(inputStream);
                String response = respScanner.next();
                //4、把响应显示到界面上
                System.out.println(response);

            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

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

测试结果:

计算机网络——基于UDP与TCP网络编程

 下期见啦!!!


计算机网络——基于UDP与TCP网络编程