Netty Review - 从BIO到NIO的进化推演

文章目录

  • BIO
    • DEMO 1
    • DEMO 2
    • 小结论
    • 单线程BIO的缺陷
    • BIO如何处理并发
    • 多线程BIO服务器的弊端
  • NIO
    • NIO要解决的问题
    • 模拟NIO
      • 方案一: (等待连接时和等待数据时不阻塞)
      • 方案二(缓存Socket,轮询数据是否准备好)
      • 方案二存在的问题
    • NIO是如何解决这些问题的
    • 使用select/poll/epoll和直接在应用层做轮询的区别
      • select底层逻辑
      • poll的底层逻辑
      • epoll的底层逻辑

在这里插入图片描述


BIO

要讲明白BIO和NIO,首先我们应该自己实现一个简易的服务器,单线程即可。

DEMO 1

package com.artisan.bio;import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;/*** @author 小工匠* @version 1.0* @mark: show me the code , change the world*/
public class Server {public static void main(String[] args) {// BIO 面向字节byte[] bytes = new byte[1024];ServerSocket serverSocket = null;try {serverSocket = new ServerSocket(1234);System.out.println("服务端已开启端口");while (true) {System.out.println();System.out.println("服务端等待连接......");Socket socket = serverSocket.accept();System.out.println("服务端已经收到连接请求");System.out.println("服务端等待数据.....");socket.getInputStream().read(bytes);System.out.println("服务端等已收到数据.....");String msg = new String(bytes);System.out.println("接收到了数据: " + msg);}} catch (IOException e) {throw new RuntimeException(e);}}
}

创建了一个服务端类,在类中实现实例化了一个SocketServer并绑定了1234端口。之后调用accept方法来接收连接请求,并且调用read方法来接收客户端发送的数据。最后将接收到的数据打印。

package com.artisan.bio;import java.io.IOException;
import java.net.Socket; /*** @author 小工匠* @version 1.0* @mark: show me the code , change the world*/
public class Client {public static void main(String[] args) throws IOException {Socket socket = new Socket("127.0.0.1",1234);socket.getOutputStream().write("数据数据数据".getBytes());socket.close();}
}

客户端,首先实例化Socket对象,并且绑定ip为127.0.0.1(本机),端口号为1234,调用write方法向服务器发送数据

运行测试会发现

在服务器启动后,客户端还没有连接服务器时,服务器由于调用了accept方法,将一直阻塞,直到有客户端请求连接服务器


DEMO 2

客户端的逻辑主要是:建立Socket –> 连接服务器 –> 发送数据,我们的数据是在连接服务器之后就立即发送的,现在我们来对客户端进行一次扩展,当我们连接服务器后,不立即发送数据,而是等待控制台手动输入数据后,再发送给服务端

客户端代码如下

 try {Socket socket = new Socket("127.0.0.1",1234);String message = null;Scanner sc = new Scanner(System.in);message = sc.next();socket.getOutputStream().write(message.getBytes());socket.close();sc.close();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}

小结论

在这里插入图片描述

从上面的运行结果中我们可以看到,服务器端在启动后:

1)首先需要等待客户端的连接请求(第一次阻塞)

2)如果没有客户端连接,服务端将一直阻塞等待;

3)然后当客户端连接后,服务器会等待客户端发送数据(第二次阻塞)

4)如果客户端没有发送数据,那么服务端将会一直阻塞等待客户端发送数据。


服务端从启动到收到客户端数据的这个过程,将会有两次阻塞的过程:

  • 1)第一次在等待连接时阻塞;
  • 2)第二次在等待数据时阻塞。

BIO会产生两次阻塞,这就是BIO的非常重要的一个特点


单线程BIO的缺陷

当我们的服务器接收到一个连接后,并且没有接收到客户端发送的数据时,是会阻塞在read()方法中的,那么此时如果再来一个客户端的请求,服务端是无法进行响应的。换言之:在不考虑多线程的情况下,BIO是无法处理多个客户端请求的


BIO如何处理并发

单线程版的BIO并不能处理多个客户端的请求,那么如何能使BIO处理多个客户端请求呢?

我们只需要在每一个连接请求到来时,创建一个线程去执行这个连接请求,就可以在BIO中处理多个客户端请求了,这也就是为什么BIO的其中一条概念是服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理。

【多线程BIO版本简易实现】

 package com.artisan.bio;import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;/*** @author 小工匠* @version 1.0* @mark: show me the code , change the world*/
public class ServerMultThread {public static void main(String[] args) {byte[] buffer = new byte[1024];try {ServerSocket serverSocket = new ServerSocket(1234);System.out.println("服务器已启动并监听8080端口");while (true) {System.out.println();System.out.println("服务器正在等待连接...");Socket socket = serverSocket.accept();new Thread(() -> {System.out.println(Thread.currentThread().getName() + " 服务器已接收到连接请求...");System.out.println();System.out.println(Thread.currentThread().getName() + "服务器正在等待数据...");try {socket.getInputStream().read(buffer);} catch (IOException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "服务器已经接收到数据");System.out.println();String content = new String(buffer);System.out.println(Thread.currentThread().getName() + "接收到的数据:" + content);}).start();}} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();}}
}

多启动几个客户端,测试结果如下

在这里插入图片描述


多线程BIO服务器的弊端

多线程BIO服务器虽然解决了单线程BIO无法处理并发的弱点,但是也带来一个问题:如果有大量的请求连接到我们的服务器上,但是却不发送消息,那么我们的服务器也会为这些不发送消息的请求创建一个单独的线程,那么如果连接数少还好,连接数一多就会对服务端造成极大的压力

所以:如果这种不活跃的线程比较多,我们应该采取单线程的一个解决方案,但是单线程又无法处理并发,这就陷入了一种很矛盾的状态,于是就有了NIO


NIO

NIO要解决的问题

我们先来看看单线程模式下BIO服务器的代码,其实NIO需要解决的最根本的问题就是存在于BIO中的两个阻塞,分别是等待连接时的阻塞和等待数据时的阻塞

package com.artisan.bio;import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;/*** @author 小工匠* @version 1.0* @mark: show me the code , change the world*/
public class Server {public static void main(String[] args) {// BIO 面向字节byte[] bytes = new byte[1024];ServerSocket serverSocket = null;try {serverSocket = new ServerSocket(1234);System.out.println("服务端已开启端口");while (true) {System.out.println();System.out.println("服务端等待连接......");Socket socket = serverSocket.accept();System.out.println("服务端已经收到连接请求");System.out.println("服务端等待数据.....");socket.getInputStream().read(bytes);System.out.println("服务端等已收到数据.....");String msg = new String(bytes);System.out.println("接收到了数据: " + msg);}} catch (IOException e) {throw new RuntimeException(e);}}
}

如果单线程服务器在等待数据时阻塞,那么第二个连接请求到来时,服务器是无法响应的。如果是多线程服务器,那么又会有为大量空闲请求产生新线程从而造成线程占用系统资源,线程浪费的情况。

那么我们的问题就转移到,如何让单线程服务器在等待客户端数据到来时,依旧可以接收新的客户端连接请求


模拟NIO

如果要解决上文中提到的单线程服务器接收数据时阻塞,而无法接收新请求的问题,那么其实可以让服务器在等待数据时不进入阻塞状态,问题不就迎刃而解了吗?

方案一: (等待连接时和等待数据时不阻塞)

package com.artisan.bio;/*** @author 小工匠* @version 1.0* @mark: show me the code , change the world*/import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;public class MyServer {public static void main(String[] args) throws Exception {ByteBuffer byteBuffer = ByteBuffer.allocate(1024);try {//Java为非阻塞设置的类ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.bind(new InetSocketAddress(1234));//设置为非阻塞serverSocketChannel.configureBlocking(false);while (true) {SocketChannel socketChannel = serverSocketChannel.accept();if (socketChannel == null) {//表示没人连接System.out.println(Thread.currentThread().getName() + " 正在等待客户端请求连接...");Thread.sleep(5000);} else {System.out.println(Thread.currentThread().getName() + "当前接收到客户端请求连接...");}if (socketChannel != null) {//设置为非阻塞socketChannel.configureBlocking(false);byteBuffer.flip();//切换模式  写-->读int effective = socketChannel.read(byteBuffer);if (effective != 0) {String content = Charset.forName("utf-8").decode(byteBuffer).toString();System.out.println(content);} else {System.out.println(Thread.currentThread().getName() + "当前未收到客户端消息");}}}} catch (IOException e) {e.printStackTrace();}}
}

客户端测试代码

package com.artisan.bio;import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;/*** @author 小工匠* @version 1.0* @mark: show me the code , change the world*/
public class ClientWithInput {private static Socket socket;private static Scanner sc;public static void main(String[] args) throws IOException {try {while (true) {socket = new Socket("127.0.0.1", 1234);String message = null;sc = new Scanner(System.in);message = sc.next();socket.getOutputStream().write(message.getBytes());}} catch (IOException e) {e.printStackTrace();} finally {socket.close();sc.close();}}
}

在这里插入图片描述

在这里插入图片描述

在这种解决方案下,虽然在接收客户端消息时不会阻塞,但是又开始重新接收服务器请求,用户根本来不及输入消息,服务器就转向接收别的客户端请求了,换言之,服务器弄丢了当前客户端的请求


方案二(缓存Socket,轮询数据是否准备好)

package com.artisan.bio;/*** @author 小工匠* @version 1.0* @mark: show me the code , change the world*/import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;public class MyServer {public static void main(String[] args) throws Exception {ByteBuffer byteBuffer = ByteBuffer.allocate(1024);List<SocketChannel> socketList = new ArrayList<SocketChannel>();try {//Java为非阻塞设置的类ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.bind(new InetSocketAddress(1234));//设置为非阻塞serverSocketChannel.configureBlocking(false);while (true) {SocketChannel socketChannel = serverSocketChannel.accept();if (socketChannel == null) {//表示没人连接System.out.println(Thread.currentThread().getName() + "正在等待客户端请求连接...");Thread.sleep(5000);} else {System.out.println(socketChannel.getRemoteAddress() + "当前接收到客户端请求连接...");socketList.add(socketChannel);}for (SocketChannel socket : socketList) {socket.configureBlocking(false);int effective = socket.read(byteBuffer);if (effective != 0) {byteBuffer.flip();//切换模式  写-->读String content = Charset.forName("UTF-8").decode(byteBuffer).toString();System.out.println(socket.getRemoteAddress() + "接收到消息:" + content);System.out.println();byteBuffer.clear();} else {// System.out.println(socket.getRemoteAddress() + "当前未收到客户端消息");}}}} catch (IOException e) {e.printStackTrace();}}
}

客户端我们使用如下代码去模拟

package com.artisan.bio;import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;/*** @author 小工匠* @version 1.0* @mark: show me the code , change the world*/
public class ClientWithInput {private static Socket socket;private static Scanner sc;public static void main(String[] args) throws IOException {try {while (true) {socket = new Socket("127.0.0.1", 1234);String message = null;sc = new Scanner(System.in);message = sc.next();socket.getOutputStream().write(message.getBytes());}} catch (IOException e) {e.printStackTrace();} finally {socket.close();sc.close();}}
}

在这里插入图片描述

在这里插入图片描述

我们可以发现
1. 消息并没有丢失
2. server端并没有开启多线程来处理消息,均是在main线程

在解决方案一中,我们采用了非阻塞方式,但是发现一旦非阻塞,等待客户端发送消息时就不会再阻塞了,而是直接重新去获取新客户端的连接请求,这就会造成客户端连接丢失。

而在解决方案二中,我们将连接存储在一个list集合中,每次等待客户端消息时都去轮询,看看消息是否准备好,如果准备好则直接打印消息。

可以看到,从头到尾我们一直没有开启第二个线程,而是一直采用单线程来处理多个客户端的连接,这样的一个模式可以很完美地解决BIO在单线程模式下无法处理多客户端请求的问题,并且解决了非阻塞状态下连接丢失的问题。


方案二存在的问题

从刚才的运行结果中其实可以看出,消息没有丢失,程序也没有阻塞。

但是,在接收消息的方式上可能有些许不妥,我们采用了一个轮询的方式来接收消息,每次都轮询所有的连接,看消息是否准备好,测试用例中只是三个连接,所以看不出什么问题来,但是我们假设有1000万连接,甚至更多,采用这种轮询的方式效率是极低的

另外,1000万连接中,我们可能只会有100万会有消息,剩下的900万并不会发送任何消息,那么这些连接程序依旧要每次都去轮询,这显然是不合适的


NIO是如何解决这些问题的

在真实NIO中,并不会在Java层上来进行一个轮询,而是将轮询的这个步骤交给我们的操作系统来进行,他将轮询的那部分代码改为操作系统级别的系统调用(select函数,在linux环境中为epoll),在操作系统级别上调用select函数,主动地去感知有数据的socket


使用select/poll/epoll和直接在应用层做轮询的区别

NIO使用了操作系统底层的轮询系统调用 select/epoll(windows:select,linux:epoll),那么为什么不直接实现而要去调用系统来做轮询呢?


select底层逻辑

假设有A、B、C、D、E五个连接同时连接服务器,那么根据我们上文中的设计,程序将会遍历这五个连接,轮询每个连接,获取各自数据准备情况,那么和我们自己写的程序有什么区别呢?

首先:我们写的Java程序其本质在轮询每个Socket的时候也需要去调用系统函数,那么轮询一次调用一次,会造成不必要的上下文切换开销

而:Select会将五个请求从用户态空间全量复制一份到内核态空间,在内核态空间来判断每个请求是否准备好数据,完全避免频繁的上下文切换。所以效率是比我们直接在应用层写轮询要高的。

如果:select没有查询到到有数据的请求,那么将会一直阻塞(是的,select是一个阻塞函数)。如果有一个或者多个请求已经准备好数据了,那么select将会先将有数据的文件描述符置位,然后select返回。返回后通过遍历查看哪个请求有数据。


select的缺点

  • 1)底层存储依赖bitmap,处理的请求是有上限的,为1024;
  • 2)文件描述符是会置位的,所以如果当被置位的文件描述符需要重新使用时,是需要重新赋空值的;
  • 3)fd(文件描述符)从用户态拷贝到内核态仍然有一笔开销;
  • 4)select返回后还要再次遍历,来获知是哪一个请求有数据。

poll的底层逻辑

poll的工作原理和select很像,先来看一段poll内部使用的一个结构体

struct pollfd{int fd;short events;short revents;
}
  • poll同样会将所有的请求拷贝到内核态,和select一样,poll同样是一个阻塞函数,当一个或多个请求有数据的时候,也同样会进行置位,但是它置位的是结构体pollfd中的events或者revents置位,而不是对fd本身进行置位,所以在下一次使用的时候不需要再进行重新赋空值的操作。

  • poll内部存储不依赖bitmap,而是使用pollfd数组的这样一个数据结构,数组的大小肯定是大于1024的。

解决了select 1、2两点的缺点。


epoll的底层逻辑

epoll是最新的一种多路IO复用的函数。这里只说说它的特点。

  • epoll和上述两个函数最大的不同是,它的fd是共享在用户态和内核态之间的,所以可以不必进行从用户态到内核态的一个拷贝,这样可以节约系统资源。

  • 另外,在select和poll中,如果某个请求的数据已经准备好,它们会将所有的请求都返回,供程序去遍历查看哪个请求存在数据,但是epoll只会返回存在数据的请求,这是因为epoll在发现某个请求存在数据时,首先会进行一个重排操作,将所有有数据的fd放到最前面的位置,然后返回(返回值为存在数据请求的个数N),那么我们的上层程序就可以不必将所有请求都轮询,而是直接遍历epoll返回的前N个请求,这些请求都是有数据的请求。

高性能网络编程 - select、 poll 、epoll 、libevent

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【计算机网络笔记】CIDR与路由聚合

系列文章目录 什么是计算机网络&#xff1f; 什么是网络协议&#xff1f; 计算机网络的结构 数据交换之电路交换 数据交换之报文交换和分组交换 分组交换 vs 电路交换 计算机网络性能&#xff08;1&#xff09;——速率、带宽、延迟 计算机网络性能&#xff08;2&#xff09;…

日历应用程序 BusyCal mac中文版软件特点

BusyCal mac是一款日历应用程序&#xff0c;它可以帮助用户轻松地管理日程安排、事件提醒、会议安排等。BusyCal 支持 macOS 和 iOS 平台&#xff0c;并且可以与 iCloud、Google 日历、Exchange 等多种日历服务进行同步。 BusyCal mac软件特点 强大的日历功能&#xff1a;Busy…

单链表经典OJ题(三)

目录 1、反转链表 2、合并两个有序链表 3、链表的中间结点 4、环形链表的约瑟夫问题 5、移除链表元素 6、移除元素 1、反转链表 206. 反转链表 - 力扣&#xff08;LeetCode&#xff09; 翻转链表的实质就是更改当前结点的前驱结点和后继结点 假设原链表为:1->2->…

【nlp】2.4 GRU模型

GRU模型 1 GRU介绍2 GRU的内部结构图2.1 GRU结构分析2.2 Bi-GRU介绍2.3 使用Pytorch构建GRU模型2.4 GRU优缺点3 RNN及其变体1 GRU介绍 GRU(Gated Recurrent Unit)也称门控循环单元结构, 它也是传统RNN的变体, 同LSTM一样能够有效捕捉长序列之间的语义关联, 缓解梯度消失或爆…

Day29力扣打卡

打卡记录 美丽塔 II&#xff08;前后缀分解 单调栈&#xff09; 链接 大佬的题解 class Solution:def maximumSumOfHeights(self, a: List[int]) -> int:n len(a)suf [0] * (n 1)st [n] # 哨兵s 0for i in range(n - 1, -1, -1):x a[i]while len(st) > 1 and …

​TechSmith Camtasia 2024破解版功能介绍及使用教程

在现在的网络互联网时代&#xff0c;越来越多的人走上了自媒体的道路。有些自媒体人会自己在网络上录制精彩视频&#xff0c;也有一些人会将精彩、热门的电影剪辑出来再加上自己给它的配音&#xff0c;做成大家喜欢看的电影剪辑片段。相信不管大家是自己平时有独特的爱好也好、…

Django(五、视图层)

文章目录 一、视图层1.视图函数返回值的问题2.三板斧的使用结论&#xff1a;在视图文件中写视图函数的时候不能没有返回值&#xff0c;默认返回的是None&#xff0c;但是页面上会报错&#xff0c;用来处理请求的视图函数都必须返回httpResponse对象。 二、JsonReponse序列化类的…

[单片机课程设计报告汇总] 单片机设计报告常用硬件元器件描述

[单片机课程设计必看] 单片机设计报告常用描述 硬件设计 AT89C51最小系统 AT89C51是美国ATMEL公司生产的低电压&#xff0c;高性能CMOS16位单片机&#xff0c;片内含4k bytes的可反复擦写的只读程序存储器和128 bytes的随机存取数据存储器&#xff0c;期间采用ATMEL公司的高…

FreeRTOS源码阅读笔记3--queue.c

消息队列可以应用于发送不定长消息的场合&#xff0c;包括任务与任务间的消息交换&#xff0c;队列是 FreeRTOS 主要的任务间通讯方式&#xff0c;可以在任务与任务间、中断和任务间传送信息&#xff0c;发送到 队列的消息是通过拷贝方式实现的&#xff0c;这意味着队列存储…

Python入门:一文详解Python列表(List)操作方法

文章目录 前言一、创建一个列表二、访问列表中的值三、更新列表四、删除列表元素六、Python列表截取七、Python列表操作的函数和方法关于Python技术储备一、Python所有方向的学习路线二、Python基础学习视频三、精品Python学习书籍四、Python工具包项目源码合集①Python工具包②…

部署百川大语言模型Baichuan2

Baichuan2是百川智能推出的新一代开源大语言模型&#xff0c;采用 2.6 万亿 Tokens 的高质量语料训练。在多个权威的中文、英文和多语言的通用、领域 benchmark 上取得同尺寸最佳的效果。包含有 7B、13B 的 Base 和 Chat 版本&#xff0c;并提供了 Chat 版本的 4bits 量化。 模…

网络运维Day16

文章目录 Docker简介什么是容器命名空间&#xff1a; Docker 的优缺点 Docker安装Docker镜像管理什么是镜像镜像管理 Docker容器管理运行容器容器启动、停止、重启拷贝文件进入容器容器与应用 DockerfileDockerfile 语法案例 总结 Docker简介 什么是容器 容器是用来装东西的&a…

Python-Python高阶技巧:HTTP协议、静态Web服务器程序开发、循环接收客户端的连接请求

版本说明 当前版本号[20231114]。 版本修改说明20231114初版 目录 文章目录 版本说明目录HTTP协议1、网址1.1 网址的概念1.2 URL的组成1.3 知识要点 2、HTTP协议的介绍2.1 HTTP协议的概念及作用2.2 HTTP协议的概念及作用2.3 浏览器访问Web服务器的过程 3、HTTP请求报文3.1 H…

红队专题-从零开始VC++C/S远程控制软件RAT-MFC-超级终端

红队专题 招募六边形战士队员[16]超级终端(1)消息 宏的定义映射cmdshell.cpp重载 构造函数Onsize 随窗口大小事件回车键发送命令添加字符转换类 StringToTransform [17]超级终端(2)接受命令创建m_cmd c类发送 接收客户端远端进程关闭 招募六边形战士队员 一起学习 代码审计、安…

景联文科技:驾驭数据浪潮,赋能AI产业——全球领先的数据标注解决方案供应商

根据IDC相关数据统计&#xff0c;全球数据量正在经历爆炸式增长&#xff0c;预计将从2016年的16.1ZB猛增至2025年的163ZB&#xff0c;其中大部分是非结构化数据&#xff0c;被直接利用&#xff0c;必须通过数据标注转化为AI可识别的格式&#xff0c;才能最大限度地发挥其应用价…

网络运维Day17

文章目录 什么是数据库MySQL介绍实验环境准备构建MySQL服务连接数据库修改root密码 数据库基础常用的SQL命令分类SQL命令使用规则MySQL基本操作创建库创建表查看表结构 记录管理命令 数据类型数值类型 数据类型日期时间类型时间函数案例枚举类型 约束条件案例修改表结构添加新字…

C++二分查找算法:最大为 N 的数字组合

涉及知识点 二分查找 数学 题目 给定一个按 非递减顺序 排列的数字数组 digits 。你可以用任意次数 digits[i] 来写的数字。例如&#xff0c;如果 digits [‘1’,‘3’,‘5’]&#xff0c;我们可以写数字&#xff0c;如 ‘13’, ‘551’, 和 ‘1351315’。 返回 可以生成的…

基于群居蜘蛛算法优化概率神经网络PNN的分类预测 - 附代码

基于群居蜘蛛算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于群居蜘蛛算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于群居蜘蛛优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神…

python语言的由来与发展历程

Python语言的由来可以追溯到1989年&#xff0c;由Guido van Rossum&#xff08;吉多范罗苏姆&#xff09;创造。在他的业余时间里&#xff0c;Guido van Rossum为了打发时间&#xff0c;决定创造一种新的编程语言。他受到了ABC语言的启发&#xff0c;ABC语言是一种过程式编程语…

PHP 服装销售管理系统mysql数据库web结构layUI布局apache计算机软件工程网页wamp

一、源码特点 PHP 服装销售管理系统是一套完善的web设计系统mysql数据库 &#xff0c;对理解php编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。 php服装销售管理系统1 二、功能介绍 (1)员工管理&#xff1a;对员工信息…