网络编程(UDP\TCP回显服务器)

news2024/12/28 3:39:14

目录

套接字socket

TCP和UDP特点比较 

特点

比较

UDP回显服务器/客户端的编写

UDP的socket api

回显服务器

客户端

TCP回显服务器/客户端的编写

TCP的socket api

回显服务器

客户端

优化服务器

1.关闭服务器创建的socket对象

2.引入线程池,为多个客户端提供服务 


套接字socket

操作系统提供的网络编程的API称为“socket api”,在传输层中,TCP和UDP两种协议的特点和差异非常大,操作系统中就提供了两套api来分别表示:

流式套接字 -> TCP使用

数据报套接字 -> UDP使用

除此之外操作系统还有其他的关于网络编程的API,比如Unix域套接字,只是本地主机上的进程和进程之间的通信方式,不能跨主机通信,现在很少使用。

TCP和UDP特点比较 

特点

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

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

比较

有连接vs无连接

有连接则是通信双方保存对方的信息,删除信息则断开连接,无连接则是通信双方不需要保存各自信息。在计算机中,各自保存双方的信息,就认为是建立了一个抽象的连接。

可靠传输vs不可靠传输

可靠 != 安全,可靠值要传输的数尽可能的全部传输给对方,在网络通信过程中,可能会存在多种意外情况,比如丢包,丢包的过程是随机的,无法预知。为了对抗丢包,引入了可靠传输特点,TCP具体这一特点,内部提供了一系列机制来实现可靠传输,UDP则是不可靠传输,传输数据时不会关心数据是否到达,接收方是否收到。 

面向字节流vs面向数据报:

TCP和文件操作都是面向字节流的,读写操作非常灵活。UDP面向数据报,传输的基本单位是一个个UDP数据报,每次读写只能读写一个完整的UDP数据报。

全双工vs版双工:

全双工:一条链路能够进行双向通信(TCP/UDP都是),在;一条链路上既可以接收也可以发送。

半双工:一条链路,只能进行单向通信,在Linux中,系统提供的一种软件资源:管道,就是半双工,接收和发送不能同时进行。

UDP回显服务器/客户端的编写

UDP的socket api

java对于系统提供的网络编程api(socket api)进行了进一步封装,进行UDP网络编程代码编写时,需要重点理解两个类:

(1)DatagramSocket:这个类是对操作系统socket概念的封装,系统中的socket可以理解为文件,socket文件可以视为网卡这个设备的抽象表示形式,针对socket文件的读写操作课相当于对网卡这个硬件设备进行读写。其实之前学习的普通文件,就是对硬盘这个硬件设备的抽象,直接操作硬盘不方便,借助文件进行操作就可以很方便的完成。类似于电视机的遥控器,通过遥控器来使用电视剧更加方便。

计算机中对具有”遥控属性“这样概念的叫做句柄(handle)。

(2)DatagramPacket:针对UDP数据报的抽象表示,一个DatagramPacket对象,就相当与一个UDP数据报,一次发送/一次接收就是传输了一个DatagramPacket对象。

回显服务器

Echo称为回显,正常服务器发送不同请求就会有不同响应,回显服务器就是请求发了什么,响应就是啥,这个过程不涉及计算和逻辑业务,是最简单的客户端服务器程序。

 编写服务器程序时,首先需要确定端口号,客户端是主动的一方,服务器是被动的一方,客户端需要找到服务器在哪。

IP地址(服务器所在主机的IP地址),port端口号(一个主机上,有多个程序都要进行网络通信,需要把那个程序用的哪个端口号记录下来,并要确保一个端口号不能被两个或多个进程关联)。

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

public class UdppEchoServer {
    private DatagramSocket socket = null;
    public UdppEchoServer(int port) throws IOException {
        socket = new DatagramSocket(port);
    }
}

可以看到抛出的一异常可以是IOException,也就是说明网络编程的本质是IO操作。

接下来要让服务器可以不停的处理请求,不停的返回响应:

第一步:读取请求并对请求进行解析,构造一个数据报类的实例,对客户端发送的数据进行接收,放入实例中,对数据进行解析,最后为了方便打印,将数据报中的二进制数拿出来,转换为String类型的数据,String有一个构造方法,通过字节数组来构造。

 DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
 socket.receive(requestPacket);
 String  request = new String(requestPacket.getData(),0, requestPacket.getLength());

第二步:根据请求返回响应,由于是回显服务器,计算请求的方法直接返回就行,返回的响应使用String进行接收。

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

第三步:将响应返回给客户端,发送时需要知道接收对象的IP和端口号,可以通过接收的UDP数据报拿到发送客户端的信息,拿到之后放到响应的数据报中使用send()方法发送,最后在服务器上面打印关键信息。

//把响应写回到客户端
//构造UDP数据包
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),0,response.getBytes().length,
                requestPacket.getSocketAddress());
//发送请求到客户端
 socket.send(responsePacket);
 System.out.printf("[%s %d] req = %s, resp =%s\n",requestPacket.getAddress(),requestPacket.getPort(),request,response);

让程序启动后不断运行,加上while循环,一个完整的UDP回显服务器就此完成了。

客户端

客户端需要有自己的端口号和ip地址,客户端和服务器在一台主机上时就可以写本地环回地址(127.0.0.1),端口号是服务器在创建socket时指定的端口号。

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverPort;
    private String serverIP;
    public UdpEchoClient(String serverPort,String serverIP) throws SocketException {
        socket = new DatagramSocket();
        this.serverPort = serverPort;
        this.serverIP = serverIP;
    }
}

 在创建socket对象时并没有指定端口号,这是因为操作系统会自动分配一个端口号,这个自动的端口号每次重启都会不一样。

服务器需要固定端口号,而客户端需要让系统自动分配:

(1)服务器要有固定端口号,是因为客户端需要主动给服务器发请求,如果服务器端口号不是固定的,客户端就会不知道把请求发给谁了。

(2)客户端需要系统自动分配,指定固定的端口号是不行的,指定客户端的端口号,可能会和客户端所在电脑上的其他程序冲突,一旦端口冲突,就会导致程序启动不了。服务器在自己手里,就算端口冲突也是可以调整的,但客户端不在本机上时,端口冲突难以解决。

客户端逻辑在编写时和服务器有相同之处,客户端发送请求到服务器后,使用UDP数据报来接受响应。

import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
    private DatagramSocket socket = null;
    private int serverPort;
    private String serverIP;
    public UdpEchoClient(String serverIP,int serverPort) throws SocketException {
        socket = new DatagramSocket();
        this.serverPort = serverPort;
        this.serverIP = serverIP;
    }
    public void start() throws IOException {
        System.out.println("客户端启动");
        Scanner scanner = new Scanner(System.in);
        while (true) {
           //输入请求
            System.out.println("请输入请求: ");
            String request = scanner.next();
            //构造请求
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),0,request.getBytes().length,
                    InetAddress.getByName(serverIP),serverPort);
            //发送请求
            socket.send(requestPacket);
            //构造接收的数据报
            DatagramPacket responsePack = new DatagramPacket(new byte[4096],4096);
            //接收服务器返回的结果
            socket.receive(responsePack);
            //转换成String类型
            String response = new String(responsePack.getData(),0, responsePack.getLength());
            System.out.println(response);
        }
    }

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

 服务器与客户端同时启动,客户端发送消息,服务器都会回显回来,并在控制台打印相应的ip地址,端口号,请求和响应:

TCP回显服务器/客户端的编写

TCP的socket api

主要涉及两个类:ServerSocket和Socket。

ServerSocket是专门提供给服务器使用的,ServerSocket在 实例化时调用的构造方法中自带端口号,实例化的对象包含一个accept方法,是一个类似接通功能的方法。

Socket是给客户端和服务器都提供服务,通过Socket的构造方法能够和指定的服务器建立连接。

TCP中通过使用InputStream和OutputStream进行文件读写操作,通过两个get方法来获取socket内部流对象。

Socket socket = new Socket();
socket.getInputStream();
socket.getOutputStream();

Tcp是字节流传输,传输基本单位是字节。

ServerSocket和Socket的功能不同:对于服务器来说,需要上来与客户端建立连接,建立连接要使用ServerSocket对象的accept方法,方法的返回值为socket类型;服务器一启动就会执行到建立连接的位置,如果此时没有客户端连接那么accept方法就会进入阻塞状态,直到有客户端连接。也就是说,ServerSocket是用与建立连接使用的,连接后将socket对象交给socket进行处理。

回显服务器

 每次创建一个服务器对象都要创建一个SerrverSocket来连接客户端,创建服务器时要包含 端口号,否则客户端没有端口号就无法连接。

public class TcpEchoServer {
    private ServerSocket serverSocket = null;

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

 在服务器中创建start方法用来启动服务器,调用start方法后服务器开始不断的执行客户端的请求,不停的处理客户端的请求就需要不停的与客户端建立连接,通过调用process()方法去处理客户端的请求,将和客户端建立连接的信息使用socket接收,传递给process方法:

public void start() throws IOException {
        System.out.println("服务器启动");
        while (true) {
            Socket socket = serverSocket.accept();
            possess(socket);
        }
    }

 实现process方法,要注意TCP是面向字节流传输,此时进行读写操作时需要使用InputStream和OutputStream,这两个类在使用完后必须回收防止资源泄露,避免数据丢失,使用 try-catch方法里的try with source用法,把对象放到try()中,使用 完毕会自动回收资源。获得数据后可以使用Read类读取请求,但是Read类读取请求后得到的是byte数组,需要进一步转换成字符串 ,这里使用Scanner去读取可以直接转换成String类型:

    private void possess(Socket clientSocket)  {
        System.out.println("客户端上线");
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()) {
            Scanner scanner = new Scanner(inputStream);
            while (true) {
                String request = scanner.next();
            }
        }catch (IOException e) {
            e.printStackTrace();
        }
    }

使用Scanner类读取数据时,读到"空白符"(空白符是一类字符的统称,包括但不限于换行,回车,空格,制表符,翻页符等)才会读取完毕 ,客户端在发送数据时,务必在每个请求的末尾加上空白符。

由于TCP是按照字节来传输的,在实际传输中,应该使若干个字节构成一个“应用层的数据报”,此时就可以通过使用空白符作为“分割符”,来区分不同的数据报。

 将读取的请求交给处理请求的函数,使用的是process函数,这里对请求的处理实际上是直接返回请求(回显服务器):

String request = scanner.next();
String response = possessFun(request);
outputStream.write(response.getBytes());

在读取请求之前,应该判断文件是否有输入 ,可以在进入while循环之后使用if语句判断请求是否有输入,使用scanner的next方法来判断,请求到达后,并且带有明确的分隔符就会返回true,如果TCP断开连接,就会返回false,使用scanner读取到文件末尾或者TCP断开连接就会返回false,否则就会阻塞等待客户端继续发送请求:

Tcp断开连接->阻塞解除返回false。

Tcp没有断开连接->对方们没有发数据过来,阻塞等待。

客户端发送请求 -> 接触阻塞并返回true。

服务器完整代码:

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

public class TcpEchoServer {
    private ServerSocket serverSocket = null;

    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true) {
            Socket socket = serverSocket.accept();
            possess(socket);
        }
    }
    private void possess(Socket clientSocket)  {
        System.out.println("客户端上线");
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()) {
            Scanner scanner = new Scanner(inputStream);
            while (true) {
                if(!scanner.hasNext()) {
                    System.out.println("客户端下线");
                    break;
                }
                String request = scanner.next();
                String response = possessFun(request);
                outputStream.write(response.getBytes());
            }
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
    private String possessFun(String request) {
        return request+" \n";
    }
}

客户端

 客户端在构造方法中应该有服务器的ip地址和端口号,在构造过程中就和服务器建立连接。

    private Socket socket = null;
    public TcpClientSocket(String address,int  port) throws IOException {
        socket = new Socket(address,port);
    }

建立start方法去启动客户端输入请求,同样使用try-catch语句实例化字节流对象,使用Scanner在控制台上输入请求,将接收的语句发送给服务器之前使用‘/n’作为分割符 ,发送给服务器使用OutputStream,传输的单位是字节,将请求转换为byte进行发送。

    public void start() {
        System.out.println("'客户端启动");
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            Scanner scanner = new Scanner(System.in);
            Scanner Rescanner = new Scanner(inputStream);
            while (true) {
                String request = scanner.next();
                request += "/n";
                outputStream.write(request.getBytes());
            }
        }

然后如同服务器一样,判断来自服务器的响应是否到达,使用scanner接收响应,在将接收的响应打印到控制台,客户端就完成编写:

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

public class TcpClientSocket {
    private Socket socket = null;
    public TcpClientSocket(String address,int  port) throws IOException {
        socket = new Socket(address,port);
    }
    public void start() {
        System.out.println("'客户端启动");
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            Scanner scanner = new Scanner(System.in);
            Scanner Rescanner = new Scanner(inputStream);
            while (true) {
                String request = scanner.next();
                request += "/n";
                outputStream.write(request.getBytes());
                if(!Rescanner.hasNext()) {
                    break;
                }
                String response = Rescanner.next();
                System.out.println(response);
            }
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 步骤:

 1.客户端从控制台得到请求。2.将请求发送到服务器3.服务器接收请求4.服务器处理请求5服务器.将响应发送给客户端6.客户端接收响应7.将响应输出在控制台。

优化服务器

1.关闭服务器创建的socket对象

serverrSocket不需要特别关闭,因为生命周期是伴随整个服务器进程。客户端的socket也是如此,但是服务器用于接收客户端信息的socket就必须关闭,服务器会对应多个客户端,如果使用完毕后不关闭当前资源文件得不到释放,就会引起文件资源泄露。在服务器代码中加上finally语句释放socket对象。

2.引入线程池,为多个客户端提供服务 

主线程处理accept,每次接收一个accept就创建一个线程来服务,这里使用可扩容的线程池:

    public void start() throws IOException {
        System.out.println("服务器启动");
        //自动扩容线程池
        ExecutorService pool = Executors.newCachedThreadPool();
        while (true) {
            Socket socket = serverSocket.accept();
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        possess(socket);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }
    }

文章到这里就结束了,感谢观看。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2255390.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

系统监控——分布式链路追踪系统

摘要 本文深入探讨了分布式链路追踪系统的必要性与实施细节。随着软件架构的复杂化,传统的日志分析方法已不足以应对问题定位的需求。文章首先解释了链路追踪的基本概念,如Trace和Span,并讨论了其基本原理。接着,文章介绍了SkyWa…

burp常用机漏洞测试理论

声明! 学习视频来自B站up主 **泷羽sec** 有兴趣的师傅可以关注一下,如涉及侵权马上删除文章,笔记只是方便各位师傅的学习和探讨,文章所提到的网站以及内容,只做学习交流,其他均与本人以及泷羽sec团队无关&a…

docker学习笔记(四)--DockerFile

文章目录 一、什么是Dockerfile二、docker build命令三、dockerfile指令3.1 FROM3.2 ENV3.3 WORKDIR3.4 RUN3.5 CMD3.6 ENTRYPOINT3.7 EXPOSE3.8 ARG3.9 ADD3.10 COPY3.11 VOLUME 四、dockerfile示例 一、什么是Dockerfile Dockerfile 是用于构建 Docker 镜像的脚本文件&#…

创造未来:The Sandbox 创作者训练营如何赋能全球创造者

创作者训练营让创造者有能力打造下一代数字体验。通过促进合作和提供尖端工具,The Sandbox 计划确保今天的元宇宙是由一个个创造者共同打造。 2024 年 5 月,The Sandbox 推出了「创作者训练营」系列,旨在重新定义数字创作。「创作者训练营」系…

基于Pyhton的人脸识别(Python 3.12+face_recognition库)

使用Python进行人脸编码和比较 简介 在这个教程中,我们将学习如何使用Python和face_recognition库来加载图像、提取人脸编码,并比较两个人脸是否相似。face_recognition库是一个强大的工具,它基于dlib的深度学习模型,可以轻松实…

抽象工厂模式的理解和实践

在软件开发中,设计模式是解决常见问题的最佳实践。抽象工厂模式是一种创建型设计模式,提供了一种创建一系列相关或相互依赖对象的接口,而无需指定它们的具体类。本文将详细解释抽象工厂模式的概念、结构、优点、缺点,并通过Java代…

Hadoop生态圈框架部署 伪集群版(五)- HBase伪分布式部署

文章目录 前言一、Hbase伪分布式部署(手动部署)1. 下载Hbase2. 上传安装包3. 解压HBase安装包4. 配置HBase配置文件4.1 修改hbase-env.sh配置文件4.2 修改hbase-site.xml配置文件4.3 修改regionservers配置文件4.4 删除hbase中slf4j-reload4j-1.7.33.jar…

【css】基础(二)

本专栏内容为:前端专栏 记录学习前端,分为若干个子专栏,html js css vue等 💓博主csdn个人主页:小小unicorn ⏩专栏分类:css专栏 🚚代码仓库:小小unicorn的代码仓库🚚 &a…

链式设计模式

链式设计模式——装饰器模式和职责链模式 装饰模式 定义: 指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式。 结构 装饰(Decorator)模式中的角色: 抽…

电子电气架构 --- E/E(电子电气架构)的重新定义

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 所谓鸡汤,要么蛊惑你认命,要么怂恿你拼命,但都是回避问题的根源&…

app-1 App 逆向环境准备(mumu模拟器+magisk+LSPosed+算法助手+抓包(socksDroid+charles)+Frida环境搭建

一、前言 本篇是基于 mumu模拟器 进行环境配置记录。(真机的后面博客记录) 二、mumu模拟器magiskLSPosed算法助手 2.1、mumu模拟器 选择 mumu 模拟器,下载地址:https://mumu.163.com 安装完成后打开,找到设置中心进…

【服务器问题】xshell 登录远程服务器卡住( 而 vscode 直接登录不上)

打开 xshell ssh 登录远程服务器:卡在下面这里,迟迟不继续 当 SSH 连接卡在 Connection established. 之后,但没有显示远程终端提示符时,这通常意味着连接已经成功建立,说明不是网络连接和服务器连接问题,…

Python Selenium 各浏览器驱动下载与配置使用(详细流程)

1、安装 pip install selenium 2、浏览器驱动下载 Chrome(google)浏览器驱动 下载地址:http://chromedriver.storage.googleapis.com/index.html 或 https://sites.google.com/a/chromium.org/chromedriver/home . 下载地址:http://chromedriver.stor…

Redis的认识

目录 1. Redis的特性2. Redis的使用场景2.1 Redis可以做什么?2.2 Redis不可以做什么 3. Redis的安装和启动4. Redis的基本全局命令4.1 GET和SET命令4.2 KEYS指令4.3 EXISTS指令4.4 DEL指令4.5 EXPIRE指令4.6 TTL指令4.7 TYPE指令 5. 数据类型和内部编码6. 单线程架构…

IntelliJ+SpringBoot项目实战(28)--整合Beetl模板框架

在前面的文章里介绍过freemarker,thymeleaf模板引擎,本文介绍另一个性能超高的模板引擎---Beetl,据说此模板引擎的性能远超Freemarker。官网的说法是,Beetl 远超过主流java模板引擎性能(引擎性能5-6倍于FreeMarker,2倍…

全面解析DApp开发中的智能合约设计

在DApp的开发过程中,智能合约的设计起到了至关重要的作用。智能合约是运行在区块链上的程序,负责处理和执行DApp中的逻辑、交易和数据存储。下面我们将深入探讨智能合约的设计原则、挑战和优化方法,帮助开发者掌握如何设计高效、安全的智能合…

利用Grounding DINO进行自动标注——目标检测任务——YOLO格式

关于Grounding DINO的环境搭建可以参考我的以前的博客,链接如下所示 如何在Linux上离线部署Grounding DINO-CSDN博客 这个博客主要来介绍如何利用Grounding DINO这个项目去进行目标检测的自动化标注。并且给出了相关的代码已经实验验证。 1.数据集准备 2. 开始实…

1.使用docker 部署redis Cluster模式 集群3主3从

1.使用docker 部署redis Cluster模式 集群3主3从 1.1 先安装docker 启动docker服务,拉取redis镜像 3主3从我们要在docker启动6个容器docker run --name redis-node-1 --net host --privilegedtrue -v /data/redis/share/redis-node-1:/data redis:6.0.8 --cluster-…

开发类似的同款小程序系统制作流程

很多老板想要开发一款和别人家类似的同款小程序系统,但是不知道该怎么开发制作,本文就为大家详细介绍一下开发类似的同款小程序的流程为大家做参考。 一、前期准备找到对标小程序:首先,需要找到你想要模仿的同款小程序&#xff0…

Jenkins凭据管理及使用详解

简介:Jenkins凭据管理是指对Jenkins中存储的敏感信息进行管理的功能,这些信息通常用于认证和授权,以确保Jenkins能够安全地与其他系统和服务进行交互。以下是关于Jenkins凭据管理添加及作用的详细介绍: 一、Jenkins凭据管理的添加 进入凭据管理页面: 登录Jenkins后,点击…