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

news/2024/5/18 14:01:10 标签: 网络, tcp/ip, udp

目录

一、什么是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数据报

656fea13032942d2803b376047622ce5.png

2、DatagramPacket API

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

 ccb9d65877df497b9f3cbbf9f7d8d641.png

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

d7f909d80197436a9ffa28a0e05c3638.png

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],这个是自己调整大小,不要太小就好了                

两次构造对象的区别:

512d19e1b469418683b64afd5382a26e.png

requestPacket.getSocketAddress()的作用:

94093b622e9e4337a850f3bc910c5aea.png

记录客户端的IP和端口号

例如:

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

 日志打印:

4bdbdf4b251a4d27bcf2ba4ac764d4b1.png


 客户端代码:

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构造:

 

整体的运行流程:

68a105caf63d45b081447c3b432d01a6.png

测试结果:

99478d8d342149a2ba81b5c2e7601ca0.gif



四、TCP网络编程简单实现


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

ServerSocket API 

26d44e259a1447eaa3a517d36ae5a506.png

 Socket API

16b59e3aad9d41f98a7e68b22fe77683.png

接下来以一个简单的回显服务器来说明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();
    }
}

测试结果:

f9feed92f3de4cd382e27307957931d1.gif

 下期见啦!!!


438e6dfc4f47434582d73d669c2c7fbe.gif


http://www.niftyadmin.cn/n/97.html

相关文章

8.2 从堆中绕过SafeS.E.H

一、实验环境 操作系统&#xff1a;windows XP SP2&#xff08;关闭DEP&#xff09; 软件版本&#xff1a;VS2008&#xff08;release&#xff09;、原版OD&#xff08;实时调试&#xff09; 二、实验代码 #include <stdafx.h> #include <stdlib.h> #include <…

MD5退出历史舞台你知道吗?

说到密码学&#xff0c;现在真的是非常的头大&#xff0c;为啥呢&#xff1f;因为密码学真的是有点难度呀&#xff0c;各种各样的加密手段&#xff0c;各种各样的解密手段&#xff0c;像 MD5 呀&#xff0c;还有 RSA 呀&#xff0c;还有 DES 呀&#xff0c;反正就是一大堆&…

C++ 多线程std::async

std::async 对于线程的创建&#xff0c;我们可以直接用thread&#xff0c;但是这会有很多的不便&#xff0c;比如获取子进程的返回值&#xff0c;解决方案是定义一个变量&#xff0c;然后将变量的指针传入到子进程中&#xff0c;然后对其进行赋值&#xff0c;但终归是不便。 除…

【PyTorch深度学习项目实战100例】—— 基于AnimeGAN模型生成宫崎骏风格动漫照片 | 第34例

前言 大家好,我是阿光。 本专栏整理了《PyTorch深度学习项目实战100例》,内包含了各种不同的深度学习项目,包含项目原理以及源码,每一个项目实例都附带有完整的代码+数据集。 正在更新中~ ✨ 🚨 我的项目环境: 平台:Windows10语言环境:python3.7编译器:PyCharmPy…

mysql left join 查询慢的问题排查

一。背景 在做一个统计功能时&#xff0c;需要关联基础信息和多个指标&#xff0c;要以基础表作为基表&#xff0c;关联各个指标&#xff0c;指标可以为空&#xff0c;所以需要使用 left join。 整体sql写完之后&#xff0c;发现执行需要5s左右&#xff0c;然后单独对 子查询进…

从 0 到 1 落地前端工程化

你将获得 初识&#xff1a;总结前端工程化技能图谱 了解&#xff1a;梳理前端工程化落地流程 掌握&#xff1a;搭建前端工程化基建项目 提高&#xff1a;实战前端工程化解决方案 作者介绍 JowayYoung&#xff0c;资深前端工程师&#xff0c;目前就职于网易互动娱乐事业群&…

Lua - windows 中执行乱码(cmd、bash、vscode)

通过 vscode 编译 a.lua 文件&#xff0c;通过 cmd 和 bash 执行 lua 来运行脚本&#xff0c;结果如下&#xff1b; C:\Users\lawsssscat\temp>more a.lua print("hh浣犲ソ")C:\Users\lawsssscat\temp>lua a.lua hh浣犲ソC:\Users\lawsssscat\temp>bashlaw…

华为面试宝典OD

目录 什么是OD&#xff1f; 目标院校 面试流程 薪资待遇 如何转正&#xff1f; 招聘实况 什么是OD&#xff1f; 官方介绍&#xff1a;OD。全称(Outsourcing Dispacth)模式&#xff0c;目前华为和德科联合招聘的简称。目前华为社招大多数是OD招聘&#xff0c;17级以下都为…