基于UDP/TCP实现客户端服务器的网络通信程序

news2024/11/28 8:52:17

目录:

前言

基于UDP实现客户端服务器的网络通信程序

基于TCP实现客户端服务器的网络通信程序


前言

网络编程的核心是Socket API,它是操作系统给应用程序提供的网络编程API,可以认为是socket api是和传输层密切相关的。在传输层里面,提供了两个最核心的协议,UDP和TCP。因此,socket api 也提供了两种风格(UDP和TCP)。

先来对UDP和TCP做一个简单的总结:

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

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

关于有无连接,可以用打电话和发微信来类比,打电话就是有连接,有无接通可以被感知,而发微信就是无连接,对方是否已经看到消息是无感知的。

关于可靠与不可靠传输,还是可以用打电话和发微信来类比,打电话就是可靠传输,对方接通并回应就是确认收到了,发微信就是不可靠传输,消息发送出去并不知道对方已经确认收到,就是不可靠传输。

面向字节流:数据传输就和文件读写类似,是“流式”的。

面向数据报:数据传输则以一个个的“数据报”为基本单位,一个数据报可能是若干个字节且带有一定格式。

全双工:一个通信通道,可以双向传输,既可以发送也可以接受。比如一根水管,是单向传输的,就是半双工。

基于UDP实现客户端服务器的网络通信程序

使用DatagramSocket这个类,表示一个socket对象。在操作系统中,把这个socket对象也是当成一个文件来处理的,相当于是文件描述符表上的一项。其中普通的文件对应的硬件设备是硬盘,socket文件对应的硬件设备是网卡。一个socket对象就可以和另外一台主机进行网络通信了,如果需要和多个不同的主机通信,则需要创建多个socket对象。

DatagramSocket(): 无参数就是没有指定端口,系统则会自动分配一个空闲的端口

DatagramSocket(int port): 传入了一个端口号,就是让当前的socket对象和这个指定的端口关联起来。从本质上来说,不是进程和端口来建立联系,而是进程中的socket对象和端口建立了联系。

DatagramPacket:表示UDP中传输的一个报文,也是UDP传输数据的基本单位。

receive(DatagramPacket p):此处传入的是一个空的对象,空的报文,receive方法内部会对参数的这个空对象进行内容填充,从而构造出数据。

send(DatagramPacket p): 将构造好的报文发送出去。

下面这里将写一个简单的回显客户端服务器,当然对于服务器来说,对于请求的处理,即业务逻辑的处理是最核心最复杂的,我们这里就回显设置,重点在于理解整个通信过程。

服务器端代码:

public class UdpEchoServer {
    // 网络编程, 本质上是要操作网卡.
    // 但是网卡在操作系统内核中,不方便直接操作,使用了"socket" 这样的文件来抽象表示网卡
    // 因此进行网络通信, 必需要先有一个 socket 对象
    private DatagramSocket socket = null;

    // 对于服务器来说, 创建 socket 对象的同时, 要让它绑定上一个具体的端口号,服务器一定要关联上一个具体的端口
    // 服务器在网络传输中才知道此时客户端的端口, 才能进行通信
    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动!");
        // 服务器不是只给一个客户端提供服务就完了,需要服务很多客户端,所以需要用到循环
        while (true){
            //1.读取客服端发来的请求
            //receive方法的参数是一个输出型参数, 需要先构造好个空白的 DatagramPacket 对象,交给 receive 来进行填充
            DatagramPacket requestPack = new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPack);
            //把数据拿出来, 构造成一个字符串
            String request = new String(requestPack.getData(),0,requestPack.getLength());

            //2.根据请求计算响应,此时为回显服务器,所以写得比较简单,请求和响应相同
            String response = process(request);

            //3.把响应写回客户端
            //send 的参数也是 DatagramPacket,也是需要把这个 Packet 对象构造好
            //此处构造的响应对象, 不能是用空的字节数组构造了, 而是要使用响应数据来构造
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPack.getSocketAddress());
            socket.send(responsePacket);

            //4.打印当前这次请求响应的处理结果
            System.out.printf("[%s:%d] req: %s; resp: %s\n",requestPack.getAddress().toString(),
                    requestPack.getPort(),request,response);
        }
    }

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

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

客户端代码:

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

    // 服务器 ip 和 端口 也需要告诉客户端. 才能顺利把消息发个服务器
    public UdpEchoClient(String serverIp,int serverPort) throws SocketException {
        socket = new DatagramSocket();
        this.serverIp = serverIp;
        this.serverPort = serverPort;
    }

    public void start() throws IOException {
        System.out.println("客户端启动!");
        Scanner scanner = new Scanner(System.in);
        while (true){
            //1.从控制台读取数据
            System.out.print("=>:");
            String request = scanner.next();
            if (request.equals("exit")){
                System.out.println("goodBye!");
                break;
            }

            //2.构造UDP请求并发送
            //构造这个 Packet 的时候, 需要把 serverIp 和 port 都传入过来,此处 IP 地址需要填写的是一个 32位的整数形式.
            //上述的 IP 地址是一个字符串,需要使用 InetAddress.getByName 来进行一个转换
            DatagramPacket requestPack = new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName(serverIp),serverPort);
            socket.send(requestPack);

            //3.读取服务器的UDP响应并解析
            DatagramPacket responsePack = new DatagramPacket(new byte[4096],4096);
            socket.receive(responsePack);
            String response = new String(responsePack.getData(),0,responsePack.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();
    }
}

通信逻辑顺序图:

基于TCP实现客户端服务器的网络通信程序

TCP提供的API主要是两个类:

ServerSocket:专给服务器使用的Socket对象

Socket:既会给服务器使用,也会给客户端使用。

TCP的传输与UDP有所不同,它并不是以数据报为单位进行传输的,而是以字节的方式,流式传输。

 // 使用这个 clientSocket 和具体的客户端进行交流
 Socket clientSocket = serverSocket.accept();

accept的作用就是接受连接,客户端在构造Socket对象的时候就会指定服务器的IP和端口,如果没有客户端来连接,此时accept就会阻塞。只要任意一个客户端连接上来,服务器这边都会创建一个Socket对象,每次创建一个Socket对象,就要占用一个文件描述符表的一个位置,因此在使用完毕后要进行释放。前面我们写的UDP就不存在这个情况,一方面是因为UDP中的Socket对象的生命周期是跟随整个程序的,声明周期更长,另一方面是Socket数量不是很多。

一次连接只能处理一个客户端的请求,那么同时有非常多的客户端发出请求,也就是高并发,此时可以用多线程来解决,为了避免线程频繁的创建和销毁,也可以用线程池。

  while (true){
            // 使用这个 clientSocket 和具体的客户端进行交流
            Socket clientSocket = serverSocket.accept();

            //多线程版本
         /*   Thread t = new Thread(()->{
                processConnection(clientSocket);
            });
            t.start();*/

            //线程池版本
            threadPool.submit(()->{
                processConnection(clientSocket);
            });
        }

但是对于特别量大的客户端,多线程和线程池也是不行的,可以多开服务器可以解决,但是毕竟成本高,还有一种方式就是IO多路复用。大概逻辑就是:给一个线程安排一个集合,这个集合里面放了一堆连接,这个线程就负责监听这个集合,哪一个连接有数据来了,这个线程就先处理哪个连接。IO多路复用本质上来说,就是充分利用等待的时间来做别的事情。

服务器端代码:

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("启动服务器!");
        // 此处使用 CachedThreadPool, 使用 FixedThreadPool的时候线程数是固定的,不太合适
        ExecutorService threadPool = Executors.newCachedThreadPool();
        while (true){
            // 使用这个 clientSocket 和具体的客户端进行交流
            Socket clientSocket = serverSocket.accept();

            //多线程版本
         /*   Thread t = new Thread(()->{
                processConnection(clientSocket);
            });
            t.start();*/

            //线程池版本
            threadPool.submit(()->{
                processConnection(clientSocket);
            });
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d]客户端上线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        // 基于上述 socket 对象和客户端进行通信
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()){
            // 由于要处理多个请求和响应, 也是使用循环来进行
            while (true){
                //1.读取请求
                Scanner scanner =new Scanner(inputStream);
                if (!scanner.hasNext()){
                    System.out.printf("[%s:%d]客户端下线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                    break;
                }
                //此处使用 next 是一直读取到换行符/空格/其他空白符结束, 但是最终返回结果里不包含上述换行符/空格/其他空白符
                String request = scanner.next();

                //2.根据请求构造响应
                String response = process(request);

                //3.返回响应结果
                PrintWriter printWriter = new PrintWriter(outputStream);
                // 此处使用 println 来写入,让结果中带有一个 \n 换行. 方便对端来接收解析
                printWriter.println(response);
                // flush 用来刷新缓冲区, 保证当前写入的数据是真正发送出去了
                printWriter.flush();

                //4.打印当前这次请求响应的处理结果
                System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(),request,response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

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

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

客户端代码:

public class TcpEchoClient {
    private Socket socket = null;
    public TcpEchoClient(String serverIp,int serverPort) throws IOException {
        socket = new Socket(serverIp,serverPort);
    }

    public void start(){
        System.out.println("客户端启动!");
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()){
            while (true){
                //1.从控制台读取数据
                System.out.print("=>:");
                String request = scanner.next();
                if (request.equals("exit")){
                    System.out.println("goodBye!");
                    break;
                }

                //2.构造请求并发送
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(request);
                printWriter.flush();

                //3.读取服务器响应
                Scanner respScanner = new Scanner(inputStream);
                String response = respScanner.next();

                //4.回显
                System.out.println(response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

总结

在上面代码中,关于有无连接,面向字节流和面向数据报,以及全双工都能在代码中体现。但是,在TCP中的可靠传输是在代码层面感知不到的。从TCP诞生的意义来说,就是为了解决可靠传输的问题的。关于TCP能够保证可靠传输问题 ,后面再来总结。

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

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

相关文章

leetcode刷题---递归思想

leetcode刷题---递归思想)1.1 递归介绍1.2 基本步骤1.3 代表题目1.3.1 入门题---青蛙跳1.3.2.1 初级题226.翻转二叉树112.路径总和1.3.3 中级题---汉诺塔问题1.3.4 进阶题---细胞分裂1.1 递归介绍 如果在函数中存在着调用函数本身的情况,这种现象就叫递…

java Resource

参看本文前 你要先了解 spring中的 Autowired和Qualifier 注解 如果之前没有接触过 可以查看我的文章 java spring 根据注解方式按(类型/名称)注入Bean 然后 创建一个java项目 引入spring注解方式 所需要的包 然后 在src下创建包 我们这里直接叫 Bean 在Bean下创建包 叫UserD…

【GIC】处理中断

目录 一、当中断变为pending时发生了什么? 二、中断响应 三、虚假的中断 四、运行优先级&抢占 五、结束中断 六、检查系统的当前状态 6.1最高优先级等待中断和运行优先级 6.2单个INTID的状态 一、当中断变为pending时发生了什么? 前面的文章…

0207 事件

事件监听事件监听版本事件类型事件概念事件在编程时系统内发生的动作或者发生的事情例子点击按钮鼠标经过拖拽鼠标事件监听(注册事件,绑定事件)让程序员检测是否有事件产生,一旦有事件触发,就立即调用一个函数做出响应…

PHP立体安全:一网打尽攻击向量

PHP立体安全:一网打尽攻击向量 所谓攻击向量,就是指黑传递有效负载或恶意结果而可以访问计算机或网络服务器的路径或方法。 PHP的安全并不只有危险函数, 这只是冰山一角 。本文将介绍PHP从汇编层面到框架层面直到标准层面的所有攻击向量。 攻…

【H5】html实现微信授权登陆

html实现微信授权登陆前言网页授权的两种 scope 的区别开发指南第一步:用户同意授权,获取code第二步:通过 code 换取网页授权access_token第三步:拉取用户信息(需 scope 为 snsapi_userinfo)代码实现:效果图总结前言 …

用Python出了3000道数学题,外甥表示要正月剪头

人生苦短,快学Python! 过年期间发现小外甥已经上小学了,我姐说老师今天给他们布置了寒假作业:每天坚持做乘法和加减法混合运算。 这我必须帮帮忙,用Python写了一段自动生成小学生计算题的代码,并支持导出…

Python入门之ChatGPT的API调(Python版)

一、Python环境部署 参考Python 环境搭建 | 菜鸟教程 Python官网:Welcome to Python.org Python文档下载地址:Our Documentation | Python.org 二、Thonny的安装 安装包地址:Thonny, Python IDE for beginners 三、ChatGPT的Key申请 网…

学Qt想系统的学习,看哪本书?

Qt 是一个跨平台应用开发框架(framework),它是用 C语言写的一套类库。使用 Qt 能为 桌面计算机、服务器、移动设备甚至单片机开发各种应用(application),特别是图形用户界面 (graphical user in…

虹科分享|在ntopng中使用多用户模式

并非所有 ntop 用户都知道 ntopng 本机实现了多用户支持。也就是说,您可以使用ntopng收集和分析来自多个用户的流量,并向每个用户显示自己的流量,隐藏其余所有流量。 您需要做的就是非常简单: 1. 启动 ntopng 并将其配置为接收受…

Python将字典转换为csv

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。喜欢通过博客创作的方式对所学的知识进行总结与归纳,不仅形成深入且独到的理…

设计模式C++实现4:装饰模式

前言 参考大话设计模式; 详细内容参见大话模式一书第六章,该书使用C#实现,本实验通过C语言实现。 装饰模式(Decorator),动态地给一个对象添加一些额外的职责,就增加功能来说,装饰…

MapReduce工作原理

一.MapReduce工作流程图 1、分片操作:FileInputstream,首先要计算切片大小,FileInputstream是一个抽象类,继承InputFormat接口,真正完成工作的是它的实现类,默认为是TextInputFormat,TextInputF…

docker一键部署网址导航+博客+管理系统(强势开源)

花森门户 在线地址(首次加载请耐心等待):http://n.huasen.cc/ 码云仓库地址:https://gitee.com/HuaSenJioJio/huasenjio-compose Github仓库地址:https://github.com/huasenjio/huasenjio-compose huasenjio 系列网站增添新作品,&…

CSCCTF-2019-Qual-FlaskLight

网页里有提示 参数为search,GET传值 测试{{7*7}} 存在SSTI模板注入,在这里简单介绍python魔法函数,与Flask内置 __class__ 返回类型所属的对象 __mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。 __bas…

Dropout的深入理解(基础介绍、模型描述、原理深入、代码实现以及变种)

目录前言一、DropOut简介1-1、DropOut论文图解1-2、DropOut介绍1-3、DropOut产生动机1-4、DropOut流程简介二、模型描述2-1、公式描述2-2、神经网络图描述2-3、一些需要注意的问题!三、Dropout代码实现以及相关变种(部分有实现)3-1、Dropout实…

Part 4 描述性统计分析(占比 10%)——上

文章目录【后续会持续更新CDA Level I&II备考相关内容,敬请期待】【考试大纲】【考试内容】【备考资料】1、统计基本概念1.1、统计学的含义及应用1.1.1、统计学的含义1.2.1、统计学的应用1.2、统计学的基本概念1.2.1、数据及数据的分类1.2.2、总体和样本1.2.3、…

体系结构原则

构建和设计软件解决方案时应考虑到可维护性。 本部分概述的原则可帮助指导你作出体系结构决策,生成简洁、可维护的应用程序。 一般而言,在这些原则的指导下构建的应用程序各部分间可通过显式接口或消息传送系统进行通信,并非松散耦合的离散组…

WinRAR自解压实现安装程序并开机自启

1、选择要打包的文件,右键添加到压缩文件,勾选“创建自解压格式压缩文件” 2、切换到高级,选择“自解压文件选项” 3、常规 - 指定解压缩路径 4、安装 - 解压缩后运行指定程序 5、模式 - 隐藏全部 全部显示:显示启动对话框&#…

bcript 算法

一、简介 今天要给大家介绍的一种“加密”算法叫做 bcrypt,bcrypt 是由 Niels Provos 和 David Mazires 设计的密码哈希函数,他是基于 Blowfish 密码而来的,并于 1999 年在 USENIX 上提出。 除了加盐来抵御 rainbow table 攻击之外&#xf…