暂时未有相关云产品技术能力~
一. 为什么要有数组?数组本质上就是让我们能 “批量” 创建相同类型的变量。例如:如果需要表示两个整型数据,那么直接创建两个变量即可 int a; int b;如果需要表示五个整型数据,那么可以创建五个变量 inta1; int a2; int a3; int a4; int a5;但是如果需要表示一万个数据, 那么就不能创建一万个变量了。这时候就需要使用数组来帮我们批量创建。注意事项:在 Java 中, 一个数组所包含的变量必须是相同类型的。二. 什么是数组?1. 基本语法定义数组有如下三种方式:int[] array1 = {1, 2, 3}; int[] array2 = new int[]{1, 2, 3}; int[] array3 = new int[10];补充说明第一、二种定义方式本质是一样的,只不过第一种简化少写了new 类型[]第三种定义方式规定了元素个数之后就不能再初始化给值,默认元素的值为0其实数组也可以写成:int arr[ ] = {1, 2, 3}; 这样就和 C 语言一样了。但是我们还是更推荐写成 int[] arr 的形式,显示说明 int 和 [ ] 是一个整体。三种写法中,最经常用到的是第一种写法。2. 数组的本质数组变量的本质是一个引用变量,而引用变量其实就是一个用来存地址的变量。因为引用变量所存储的数据是一个地址,而这个地址一般都是指向一块在堆上动态开辟的空间,这块空间才是真正存储了我们初始化出来的数据,所以我们说某个引用(变量)指向了一个对象,这个对象就是那个地址之下存储的数据。再看下面这个例子,可以帮助我们理解 数组的本质是一个引用变量 这句话的含义:int[] array1 = {1, 2, 3}; // arrar2这个引用指向了 // array1这个引用所指向的对象 int[] arrar2 = array1; System.out.println(array1); System.out.println(arrar2); --------结果如下-------- [I@1b6d3586 [I@1b6d3586解释说明:三. 数组的使用1. 获取长度、访问元素int[] arr = {1, 2, 3}; // 获取数组长度 System.out.println("length: " + arr.length); // 读取数组中的元素 System.out.println(arr[0]); // 修改数组中的元素 arr[0] = 99; System.out.println(arr[0]); --------结果如下-------- length: 3 1 99注意事项:使用数组名.length能够获取到数组的长度,数组名后的那个点是成员访问操作符。使用 [ ] 可以按下标取数组元素,需要注意 下标从 0 开始计数使用 [ ] 操作既能读取元素, 也能修改元素下标访问操作不能超出有效范围 [0, length - 1] , 如果超出有效范围, 会出现下标越界异常。2. 遍历数组所谓 “遍历” 就是指将数组中的所有元素都访问一遍,不重不漏。通常需要搭配循环语句。方法一:使用 for 循环遍历数组int[] arr = {1, 2, 3}; for(int i = 0; i < arr.length; ++i){ System.out.println(arr[i]); } --------结果如下-------- 1 2 3方法二:使用 for-each 遍历数组for-each 是 for 循环的另外一种使用方式. 能够更方便的完成对数组的遍历. 可以避免循环条件和更新语句写错int[] arr = {1, 2, 3}; // 注意:对x的修改操作并不会影响数组元素的值 for (int x : arr){ System.out.println(x); } --------结果如下-------- 1 2 3补充:使用 toString 打印数组toString 这个方法属于 Arrays 类,所以我们需要引入 Arrays 类:import java.util.Arrays;import java.util.Arrays; public class Test { public static void main(String[] args) { int[] a = {1, 2, 3}; System.out.println(Arrays.toString(a)); } } --------结果如下-------- [1, 2, 3]3. 下标越界、空引用如果抛出程序抛出了 java.lang.ArrayIndexOutOfBoundsException 这个异常,就说明数组越界了。使用数组一定要谨防下标越界。int[] arr = {1, 2, 3}; System.out.println(arr[100]); --------结果如下-------- Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 100 at Test.main(Test.java:2)如果抛出程序抛出了 Exception in thread "main" java.lang.NullPointerException 这个异常,就说我们有尝试去访问一款空地址,这是不允许的。int[] arr = null; System.out.println(arr[0]); --------结果如下-------- Exception in thread "main" java.lang.NullPointerException at Test.main(Test.java:2)4. 数组作为函数参数数组作为函数参数进行传递的时候,其实还是按值传递,只不过传递的值是一个地址,那么在函数内部对这个数组就会出现两种情况:形参修改指向。这只会影响到形参的指向形参修改指向对象的值5. 数组作为函数返回值四. 数组拷贝1. 手动拷贝下面代码不叫拷贝,因为此时只有一个数组对象:int[] arr1 = {1, 2, 3}; System.out.println(Arrays.toString(arr1)); int[] arr2 = arr1; System.out.println(Arrays.toString(arr1)); --------结果如下-------- [1, 2, 3] [1, 2, 3]真正的拷贝应该重新开辟出一块和原数组一样大的空间,然后遍历原数组的元素,把它们的值逐个拷贝到新空间:int[] arr1 = {1, 2, 3}; int[] arr2 = new int[arr1.length]; for(int i = 0; i < arr1.length; ++i){ arr2[i] = arr1[i]; } System.out.println(Arrays.toString(arr2)); --------结果如下-------- [1, 2, 3]2. 使用库函数拷贝函数原型dataType[] Arrays.copyOf(dataType[] srcArray,int length);说明Arrays 类的 copyOf() 方法是复制数组至指定长度srcArray 表示要进行复制的数组,length 表示复制后的新数组的长度使用这种方法复制数组时,默认从原数组的第一个元素(索引值为 0)开始复制,目标数组的长度将为 length。如果 length 大于 srcArray.length,则目标数组中采用默认值填充;如果 length 小于 srcArray.length,则复制到第 length 个元素(索引值为 length-1)即止。举例int[] arr1 = {1, 2, 3}; int[] arr2 = Arrays.copyOf(arr1, 3); System.out.println(Arrays.toString(arr2)); --------结果如下-------- [1, 2, 3]五. Arrays中几个常用的方法1. 对数组进行排序方法:Arrays.sort(int[] a)2. 拷贝数组方法:dataType[] Arrays.copyOf(dataType[] srcArray,int length)3. 对数组元素进行填充方法:Arrays.fill(a1, value);4. 把数组转化为字符串,方便打印方法:string Arrays.toString(dataType[] )六. 二维数组1. 定义方式和一维数组一样,二维数组的定义方式也有三种:int[][] array1 = {{1, 2, 3}, {4, 5, 6}}; int[][] array2 = new int[][]{{1, 2, 3}, {4, 5, 6}}; int[][] array3 = new int[2][];// 列数写不写都可以2. 二维数组的本质我们在学习C语言中的二维数组时,都应该听说过这句话:“二维数组是特殊的一维数组”,Java里面把这句话诠释得很好。不同于 C/C++ 中的二维数组,他们要求定义时必须确定列数,而Java中的二维数组在定义时必须确定行数,列数无所谓:// 不指定列数 int[][] array1 = new int[2][]; System.out.println(Arrays.deepToString(array1)); // 指定列数 int[][] array2 = new int[2][3]; System.out.println(Arrays.deepToString(array2)); --------结果如下-------- [null, null] [[0, 0, 0], [0, 0, 0]]3. 二维数组的三种遍历方式3.1 for 循环int[][] arr = {{1, 2, 3}, {4, 5, 6}}; for(int i = 0; i < arr.length; ++i){ for (int j = 0; j < arr[i].length; ++j){ System.out.print(arr[i][j] + " "); } System.out.println(); } --------结果如下-------- 1 2 3 4 5 63.2 for - each这里拿出的值只是数组元素的拷贝,对它们的修改并不会影响原数组的元素int[][] arr = {{1, 2, 3}, {4, 5, 6}}; for(int[] ret : arr){ for(int x : ret){ System.out.print(x + " "); } System.out.println(); } --------结果如下-------- 1 2 3 4 5 63.3 使用 Arrar.deepToString 函数int[][] arr = {{1, 2, 3}, {4, 5, 6}}; System.out.println(Arrays.deepToString(arr)); --------结果如下-------- [[1, 2, 3], [4, 5, 6]]
一. 针对没有需求的案例来设计测试用例针对没有需求的案例,我们可以从如下几个方面思考来设计测试用例功能测试 + 界面测试 + 性能测试 + 安全测试 + 兼容性测试 + 易用性测试案例一:针对一个水杯来设计测试用例案例二:针对一个登陆系统来设计测试用例二. 针对有需求的案例来设计测试用例大概设计思路如下:需求分析概括出需求有哪些功能设计测试点设计测试用例1. 穷举法假如说给定的软件需求是:提示姓名长度为6~15位。测试时数据我们设定为6、7、8 … 14、15,这样通过穷举法来设计测试用例,若测试用例通过,则认为功能符合需求要求。假如说,给定的长度不是6~15位,而是6 ~ 500位,这时应该如何设计测试用例呢?这样测试用例通过穷举法肯定是不现实的。2. 等价类概念:针对需求把输入范围内的所有测试用例划分成若干个等价类,从其中一个等价类里取出一个用例,若该测试用例测试通过,则认为该测试用例所在的等价类通过。等价类的核心是对测试数据进行分区分块,使用较少的测试用例达到符合的系统测试覆盖率。等价类又划分成有效等价类和无效等价类:有效等价类:针对需求来说是有效且有意义的数据构成的集合。无效等价类:针对需求来说是无效且没有意义的数据构成的集合。根据等价类划分测试用例的步骤:确定有效等价类和无效等价类编写测试用例举例需求:姓名可输入6~200位的字符,应该如何来设计测试用例呢?第一步:确定有效等价类和无效等价类有效等价类:6~200无效等价类:小于6大于200PS:其实在设计时还需要发散性的根据需求考虑更多情况,比如还可以针对字符的类型(数字、字符串、特殊字符)来设计有效等价类和无效等价类,这里只是简单的举例,只考虑长度。第二步:编写测试用例3. 边界值边界值法通常是对等价类的补充。注意设计边界值测试用例时需要加上:边界值 + 次边界值还是继续用等价类的例子,这次我们补充上边界值的测试用例:4. 判定表法判定表法的核心是要考虑输入输出之间的组合关系,根据这个这个关系画出判定表然后设计测试用例。判定表设计测试用例的步骤:确定输入条件和输出条件找出输入条件和输出条件之间的关系画判定表根据判定表编写测试用例举例需求:订单已提交,且订单总金额大于300元或订单有红包,则认为该订单属于有优惠的订单,否则属于没有优惠的订单。step1:确定输入条件和输出条件输入条件:金额大于300元、有红包、订单已提交输出条件:有优惠、无优惠step2:找出输入条件和输出条件之间的关系step3:画判定表step4:根据判定表来编写测试用例金额大于300元,没有红包,没有提交订单,结果为无优惠金额不大于300元,有红包,没有提交订单,结果为无优惠金额不大于300元,没有红包,提交了订单,结果为无优惠金额大于300元,有红包,没有提交订单,结果为无优惠金额大于300元,没有红包,提交了订单,结果为有优惠金额不大于300元,有红包,提交了订单,结果为有优惠金额大于300元,有红包,提交了订单,结果为有优惠金额不大于300元,没有红包,没有提交订单,结果为无优惠5. 场景设计法5.1 简介现在的软件几乎都是用事件触发来控制流程的,事件触发时的情景便形成了场景,而同一事件不同的触发顺序和处理结果就形成事件流。该方法可以比较生动地描绘出事件触发时的情景,有利于测试设计者设计测试用例,是测试用例更容易理解和执行。典型的应用是是用业务流把各个孤立的功能点串起来,为测试人员建立整体业务感觉,从而避免陷入功能细节忽视业务流程要点的错误倾向。5.2 基本设计步骤根据说明,描述出程序的基本流以及各项备选流根据基本流和各项备选流生成不同的场景对每一个场景生成相应的测试用例对生成的所有测试用例重新复审,去掉多余的测试用例测试用例确定后,对每一个测试用例确定测试数据值5.3 基本流和备选流基本流:也叫有效流或正确流,模拟用户正确的业务操作流程。备选流:也叫无效流或错误流,模拟用户错误的业务操作流程。5.4 使用场景主要用来测试软件的业务逻辑和业务流程。一般先采用等价类划分、边界值分析、错误推断法、因果图及判定表法等对单点功能进行验证,验证通过后再采用场景法进行业务流程的验证。5.5 优缺点优点:涉及到业务场景,使用场景法有利于测试设计者设计测试用例,使测试用例更容易理解和执行。缺点:只验证业务流程,不验证单点功能。5.6 实例场景介绍用户进入网上购物系统网站进行购物,选好物品后进行购买。这时需要使用账号登录,登录成功后付款,交易成功后生成订单,完成此次购物活动。第一步:分析需求,确定基本流和备选流事件第二步:根据基本流和备选流来确定场景第三步:设计用例第四步:设计测试用例中所需的数据6. 错误猜测法错误猜测法是基于对被测试软件设计的理解、过往经验以及个人直觉,推测出软件可能存在的缺陷,从而去针对性地设计测试用例。这个方法强调的是对被测试软件的需求理解以及设计实现的细节把握,还有个人的经验和直觉。错误推测法和目前流行的“探索式测试方法”的基本思想一致,这类方法在敏捷开发模式下的投入产出比很高,被广泛应运于测试。这个方法的缺点是难以系统化,并且过度依赖个人能力。以注册为例,我们可以对这个场景直接进行错误猜测,从而设计测试用例:校验中特殊字符空格的处理?密码校验中的大小写?姓名中的特殊字符?密码发送是否明文?
首先我们知道在 C/C++ 中,char类型大小为1字节,即8bit。每一个bit位可以存0和1两个数字,8个bit位一共有 2^8 种排列方法,即区间 [00000000, 11111111]如果是 unsigned char 的话,表示出来的数据取值范围是 [0, 255]如果是有符号的 char,取值范围是 [-128,127]分析一段代码int main() { char a[1000]; int i; for (i = 0; i < 1000; ++i) { a[i] = -1 - i; } printf("%d\n", strlen(a)); return 0; }最终输出结果为 255,我们来分析一下为什么首先要知道 strlen 的作用是输出字符串中 ‘/0’ 之前的字符的个数‘\0’ 的ASCLL码是数字0分析存入 a 的所有字符的ASCLL码,看存入第一个0之前有多少个字符[-1, -2 … -128, 127, 126 … 0, -1, -2 …],统计得到第一个 0 之前有 255 个字符,就是 strlen(a) 返回的结果
一. 预备知识1. IP地址因特网是在网络级进行互联的,因此,因特网在网络层(IP层)完成地址的统一工作,即将不同物理网络的地址统一到具有全球惟一性的IP地址上,IP层所用到的地址叫作因特网地址,又叫IP地址。因特网采用一种全局通用的地址格式,为每一台主机都分配一个IP地址,以此屏蔽物理网络地址的差异,即IP地址的意义就是标识公网内唯一一台主机。在IP数据包中的信息带有源IP地址和目的IP地址,它们分别标识通信的源结点和目的结点,即信源和信宿。IP数据包经由路由转发的时候,源IP和目的IP不会改变,除非做了NAT转换才能改变。2. 端口号(port)网络通信的本质是进程间通信,有了IP就可以标识公网内唯一的一台主句,想要完成网络通信我们还需要一个东西来标识一台主机上的某个进程,这个标识就是端口号(port)。端口号是传输层协议的内容,它包括如下几个特点:端口号是一个2字节,16比特位的整数。一台主机中,一个端口号只能被一个进程所占用。理解 “端口号” 和 “进程ID”我们之前在学习操作系统的时候,知道pid可以用来标识进程;此处我们的端口号也是唯一标识一个进程。那么这两者之间又存在怎样的关系呢?二者的相同点都是唯一标识主机内的一个进程,区别在于pid强调在系统的范围呢标识进程;而端口号强调在网络的范围内去标识进程。既然pid已经做到唯一标识一个进程,为何还要引入端口号呢?我们可以从生活的角度去理解这种情况:即然每个人都有了唯一标识自己的身份照号,为何学校还要给我们分配学号呢?直接用身份照号不行吗?学校给学生引入学号后,除了唯一标识学生这个作用外还有其他两个优点:方便记忆和书写。常见的学号例如:20204912,因为比较短,这样就方便记忆,而且考试的时候书写也容易。试想一下,如果以身份证代替学号,能不能正确背下来是一回事,太长了万一考试的时候记错或写错了一个数字就麻烦了。可以自己设定数字的意义,方便组织和管理。例如学号:20204912,一眼就能看出你是2020级的学生;对应的身份证号不同位置的数字也有各自的意义,但于学校管理意义不大。源端口号和目的端口号对应到网络层协议的源IP和目的IP,传输层协议(TCP和UDP)的数据段中也有两个端口号, 分别叫做源端口号和目的端口号.,它们就是在描述 “数据是那个进程发送的, 要发给另外那个进程”。3. socket网络通信socket通信的本质就是跨网络的进程间通信,任何的网络客户端和网络服务如果要进行正常的数据通信,它们必须要有自己的端口号和匹配所属主机的IP地址。4. 认识TCP/UDP协议我们进行网络编程时通常是在应用层编码,应用层下面就是传输层。应用层往下传输数据时不必担心也没有必要知道数据的传输情况如何,这个具体地交给传输层来解决,所以我们有必要简单了解一下传输层的两个重要协议TCP和UDP。TCP协议TCP全称Transmission Control Protocol,即传输控制协议,它有如下特点:属于传输层协议。有连接。可靠传输。面向字节流。UDP协议UDP全称User Datagram Protocol,即用户数据报协议,它有如下特点:属于传输层协议。无连接。不可靠传输。面向数据报。在我们的认知里一定要是安全的、稳定的才好,那传输层为什么还要引入一个不可靠传输方式的UDP协议呢?TCP协议虽然是可靠传输,但是“可靠”是要付出一些效率上的成本的,可能会导致传输速度比较慢,而且实现起来相对复杂;以这个角度去看UDP协议,虽然可能在传输过程中出现丢包的情况,但效率上是要更快的。通常两个协议可以搭配起来使用,网速快时用TCP协议,网速慢时用UDP协议,但如果是要传输重要数据的时候就应该只用TCP了。5. 网络字节序我们知道,内存中的数据权值排列相对于内存地址的大小有大端和小端之分:大端存储:高权值数字存到内存的低地址位置上,低权值存到高地址上。小端存储:高权值数字存到内存的高地址位置上,低权值存到高地址上。数据在发送时,发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序以字节为单位发出;接收主机把接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序以字节为单位保存的。即先发出低地址的数据,后发出高地址的数据;接收到的数据也是按低地址到高地址的顺序存。如果发送端和接收端主机的存储字节序不同,则会造成发送的数据和识别出来的数据不一致的问题,如下图所示:网络在传输数据时同样有大端小端之分,TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高权值,不管这台主机是大端机还是小端机,,最后都要按照TCP/IP规定的网络字节序(大端)来发送/接收数据:如果发送主机是小端机, 就需要先进行数据转换;否则忽略,直接发送即可。如果接受主机是小端机,则拿到数据后需要进行转换;否则忽略,直接读取即可。二. socket编程1. 套接字说明socket通常也称为“套接字”,程序可以通过“套接字”向网络发出请求或者响应网络请求。socket位于传输层之上、应用层之下。socket编程是通过一系列系统调用完成应用层协议。如FTP、Telent、HTTP等应用层协议都是通过socket编程来实现的。从套接字所处的位置来讲,套接字上连应用进程,下接网络协议栈,是应用程序与网络协议栈进行交互的接口。套接字是对网络中应用进程之间进行双向通信的抽象,他提供了应用层进程利用网络协议栈交换数据的机制。套接字的本质Linux和UNIX的I/O内涵是系统中的一切都是文件。当程序在执行任何形式的I/O时,程序都是在读或者在写一个文件描述符,从而实现操作文件,但是,这个文件可能是一个socket网络连接、目录、FIFO、管道、终端、外设、磁盘上的文件。一样的道理,socket也是使用标准Linux文件描述符和其他网络进程进行通信的。2. socket函数简介socket函数基本为系统调用函数,它是操作系统向网络通信进程提供的函数接口。从实现的角度来讲,套接字系列函数是一个复杂的软件模块,它包含了一定的数据结构和许多选项,由操作系统内核来管理。3. socket编程说明Linux系统是通过套接字(socket)函数来进行网络编程的。socket技术提供了在TCP/IP模型各个层上的编程支持,该技术是先在内核中处理收到的各层协议数据,然后应用程序再以文件操作的方式接收内核返回的数据。其中应用程序对文件的处理是通过一个文件描述符来进行的,socket文件描述符可以看成普通的文件描述符来进行操作,这就是Linux设备无关性的好处,可以通过对文件描述符的读写操作来实现网络间数据流的传输。重新理解IP地址与端口端口是指网络中面向连接服务和无连接服务的通信协议端口,它是一种抽象的软件结构,包括一些数据结构和I/O(基本输入/输出)缓冲区。IP地址用来标识网络中不同主机的地址,而端口号则是标识一台主机上不同网络通信进程的地址,IP地址与端口号合起来标识的就是网络中唯一的进程。如果把IP地址比作一栋旅馆,端口就是旅馆里的一个个房间。一个IP地址的端口可以有65536(即2^16)个之多。端口是通过端口号来标识的,端口号是个16byte位的整数,范围是0 ~ 65535( 2^16-1)。其中端口1~1024是系统保留端口。一次socket通信连接会涉及源IP地址、源端口、目的IP地址和目的端口四个要素。源IP地址、源端口标识的是客户端进程,其中源端口是操作系统随机分配的;目的IP地址、目的端口标识的是服务端进程,其中目的端口是由服务器程序指定的。IP地址、端口号、socket套接字三者在数据结构上的联系4. socket地址说明及转换函数4.1 三种常见的结构类型在套接字编程中,有三种常见的结构类型,它们用来存放socket地址信息。这三种结构类型分别为struct in_addr、struct sockaddr、struct sockaddr_in,对这三种结构类型说明如下,使用它们需要包含头文件#include <netinet/in.h>。struct in_addr专门用来存储IP地址,对于IPv4来说,IP地址为32位无符号整数,其定义可以在/usr/include/linux/in.h下找到,具体IP地址的值存储在成员变量s_addr中:struct sockaddr结构用来保存保存套接字的完整地址信息,其定义如下:struct sockaddr { unsigned short sa_family; /* 地址簇,AF_xxx */ char sa_data[14]; /* 14B的协议地址 */ };struct sockaddr结构中sa_family成员说明的是地址簇类型,一般为“AF_INET”;而sa_data则包含主机的IP地址和端口等信息。struct sockaddr结构类型使用在socket相关的系统调用函数中,但这个结构中的sa_data字段可以包含较多的信息,不便于实际编程和对其进行赋值,因此,又建立了struct sockaddr_in结构,该结构与struct sockaddr结构的大小相等,能更好地处理struct sockaddr结构中的数据。对struct sockaddr_in结构变量进行赋值完成后进行socket相关的系统调用函数时,再将struct sockaddr_in结构变量强制转化为struct sockaddr结构类型即可。sttruct sockaddr_in结构定义如下:在实际应用的编程中,对套接字地址结构的使用方法和流程如下:定义一个struct sockaddr_in类型的结构变量,并将它初始化为0,代码如下:struct sockaddr_in myad; memset(&myad, 0, sizeof(struct sockaddr_in));给这个结构变量赋值,代码如下:myad.sin_family = AF_INET; myad.sin_port = htons(8080); myad.sin_addr.s_addr = htonl(INADDR_ANY);在进行函数调用时,都要将这个结构强制转换为struct sockaddr类型,代码如下:bind(serverFd, (struct sockaddr*)&myad, sizeof(myad));4.2 整型数据字节序转化函数为保证“大端”和“小端”字节序机器之间能相互进行正常的网络通信,需在发送多字节的整数时,将主机字节序转换成网络字节序,或将网络字节序转换为主机字节序。字节序转换主要是针对整型数据进行的,字符型由于是单字节,所以不存在这个问题。整型整型字节序转换函数原型及其说明如下。所需头文件 #include <arpa/inet.h>函数说明 完成网络字节序与主机字节序的转换,注意已经完成转换了的整数就不要在重复转换了函数原型 uint16_t htons(uint16_t hostshort) //短整型主机转换为网络字节序uint32_t htonl(uint32_t hostlong) //长整型主机转换为网络字节序uint16_t ntohs(uint16_t netshort) //短整型网络转换为主机字节序uint32_t ntohl(uint32_t netlong) //长整型网络转换为主机字节序函数传入值 hostshort、hostlong:为转换前的主机字节序数值netshort、netlong为转换前的网络字节序数值函数返回值 ① htons、htonl返回转换后的网络字节序数值② ntohs、ntohl返回转换后的主机字节序数值附加说明 h表示主机,n表示网络,s表示短整数,l表示长整数,to表示转换4.3 IP地址转换函数IP地址转换函数是指完成点分十进制数IP地址与二进制数IP地址之间的相互转换。IP地址转换主要由inet_aton、inet_addr和inet_ntoa这三个函数完成,但它们都只能处理IPv4地址,而不能处理IPv6地址。这三个函数的函数原型及其具体说明如下。1、inet_addr函数原型 in_addr_t inet_addr(const char* cp)函数说明 将点分十进制数IP地址转换为二进制数IP地址并完成网络字节序的转换所需头文件 #include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>函数传入值 cp:点分十进制数IP地址,如“10.10.10.1”函数返回值 in_addr_t 一般为32位的unsigned int成功:返回二进制数形式的IP地址失败:返回一个常值INADDR_NONE(32位均为1)2、inet_aton函数原型 int inet_aton(const char* cp, struct in_addr* inp)函数说明 将点分十进制数IP地址转换为二进制数地址并完成网络字节序的转换所需头文件 #include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>函数传入值 cp:点分十进制数IP地址,如“10.10.10.1”inp:转换后的二进制数地址信息保存在inp中函数返回值 成功:非0失败:03、inet_ntoa函数原型 char* inet_ntoa(struct in_addr in)函数说明 将二进制数地址转换为点分十进制数IP地址所需头文件 #include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>函数传入值 in:二进制数IP地址,注意类型是struct in_addr,该数据一般从套接字地址结构中拿到函数返回值 成功:返回字符串指针,此指针指向了转换后的点分十进制数IP地址失败:NULL5. socket主要函数说明5.1 基本套接字函数PS:基本套接字函数的头文件都为:#include <sys/socket.h>、#include<sys/types.h>这两个。(1)socket函数创建套接字要用到socket这个函数,该函数的原型如下:头文件:#include <sys/socket.h>、#include<sys/types.h>函数说明:创建一个socket文件描述符。函数返回值:成功:socket文件描述符。失败:-1,失败原因存于error中。参数说明:domain:即协议簇。AF_INET:IPv4协议。AF_INET6:IPv6协议。AF_LOCAL:UNIX域协议。AF_ROUTE:路由套接口。AF_KEY:密钥套接口。type:即服务类型。SOCK_STREAM:双向可靠数据流,对应TCP。SOCK_DGRAM:双向不可靠数据报,对应UDP。SOCK_RAM:提供传输层以下的协议,可以访问内部网络的接口,例如, 接收和发送ICMP报文。protocol:即协议类型。type为SOCK_RAM时,需要设置此值说明协议类型。其他类型统一设置为0即可,内部会根据前两个参数的值自动推导出协议结果。补充:下表列出了当进行socket调用时,其中的协议簇(domain)与服务类型(type)可能产生的组合。- AF_INET AF_INET6 AF_LOCAL AF_ROUTE AF_KEYSOCK_STREAM TCP TCP Yes SOCK_DGRAM UDP UDP Yes SOCK_RAW IPv4 IPv6 Yes Yes(2)bind函数头文件:#include <sys/socket.h>、#include<sys/types.h>函数说明:将一个套接字地址与socket文件描述符联系起来。利用bind绑定地址时,可以指定主机的IP地址和端口号。此函数一般为客户端调用,我们在填充IP地址时可以使用通配地址INADDR_ANY(为宏定义,其值等于0),此时的含义是让服务器端计算机上的所有网卡的IP地址都可以作为服务器的IP地址,也即监听外部客户端程序发送到服务器端所有网卡的网络请求。参数说明:socket:socket文件描述符。address:指向sockaddr结构,该结构包含IP地址和端口等信息address_len:sockaddr结构的大小,可设置为sizeof(struct sockaddr)函数返回值:成功:0失败:-1,失败原因存于error中(3)listen函数头文件:#include<sys/socket.h>、#incldue<sys/types.h>函数说明:设置监听套接字参数说明:sockfd:需要设置为监听套接字的socket文件描述符。backlog:连接队列长度,通常设置为5或10。函数返回值:成功返回0,失败返回-1,失败原因存于error中。附加说明对于监听套接字文件描述符sockfd,内核要维护两个队列,分别为未完成连接队列和已完成连接队列,这两个队列之和不能超过backlog。(4)accept函数头文件:#include<sys/socket.h>、#include<types.h>函数说明:接受socket连接,返回一个新的socket文件描述符,原socket文件描述符仍为listen函数所用,而新的socket文件描述符用来处理连接的读写操作。参数说明:sockfd:监听套接字。addr:写入远程主机的套接字地址,是一个输出型参数。addrlen:写入addr的大小,也是一个输出型参数。函数返回值:成功:返回实际读取的字节数。失败:-1,错误码存放于error中。附加说明accept函数由TCP服务器调用,为阻塞函数,从已完成连接的队列中返回一个连接;如果该队列为空,则进程进入阻塞等待。函数返回的套接字为已新的连接套接字,监听套接字仍为listen函数所用。(5)connect函数头文件:#include<sys/socket.h>、#include<sys/types.h>函数说明:主动建立socket连接函数传入值:sockfd:socket文件描述符addr:对端主机的套接字地址addrlen:sockaddr结构的大小,可设置为sizeof(struct sockaddr)函数返回值:成功返回0,失败返回-1,失败原因存于error中。5.2 UDP读写函数UDP套接字是无连续协议,必须使用sendto函数发送数据,使用recvfrom函数接收数据,且发送时需要指明目的地地址。sendto函数与send的功能基本相同,recvfrom与recv的功能基本相同,只是sendto和recvfrom函数参数中都带有对端的地址信息,这两个函数是专门为UDP协议提供的。(1)sendto函数头文件:#include <sys/socket.h>函数说明:通过socket文件描述符发送数据到对端,用于UDP协议参数说明:socket:socket文件描述符message:发送数据的首地址length:发送数据的长度flags:该参数可以设置为以下标志的组合0:以阻塞方式发送数据。MSG_OOB:发送带外数MSG_DONTROUTE:告诉IP协议目的主机在本地网络,没有必要查找路由表MSG_DONTWAIT:设置为非阻塞操作MSG_NOSIGNAL:表示发送动作不愿被SIGPIPE信号中断dest_addr:存放目的主机的IP地址和端口信息,即socket地址dest_len:to的长度,可设置为sizeof(struct sockaddr)函数返回值:成功:实际发送的字节数失败:-1,失败原因存于error中(2)recvfrom函数头文件:#include <sys/socket.h>函数说明:通过socket文件描述符从对方接收数据,用于UDP协议参数说明:socket:文件描述符。buffer:接收数据的首地址(输出型参数)。length:需要接受数据的长度。flags:该参数可以设置为以下标志的组合。0:以阻塞方式接收数据MSG_OOB:接收带外数据MSG_PEEK:查看数据标志,返回的数据并不在系统中删除,如果再次调用该函数,会返回相同的数据内容。MSG_DONTWAIT:设置为非阻塞操作MSG_WAITALL:强迫接收到length大小的数据后才返回,除非有错误或有信号产生。address:存放发送方的IP地址和端口(输出型参数)。address_len:socket地址的长度,可设置为sizeof(struct sockaddr)。函数返回值成功:实际接收到的字节数。失败:-1,失败原因存于error中。三. UDP套接字编程UDP协议是非连接非可靠的数据传输,常用在对数据质量要求不高的场合。UDP服务器通常是非连接的,因而,UDP服务器进程不需要像TCP服务器那样在监听套接字上接收新建的连接;UDP只需要在绑定的端口上等待客户机发送来的UDP数据报文,并对其进行处理和响应。一个TCP服务进程只有在完成了对某客户机的服务后,才能为其他的客户机提供服务。而UDP服务器只是接收数据报文,处理并返回结果。UDP支持广播和多播,如果要使用广播和多播,必须使用UDP套接字。UDP套接字没有连接的建立和终止过程,UDP只需要两个分组来交换一个请求和答应。UDP不适合海量数据的传输。1. 普通UDP服务器编程模型1.1 UDP服务器端流程① 建立UDP套接字② 绑定套接字到特定的地址③ 等待并接受客户端信息④ 处理客户端请求⑤ 发送信息给客户端⑥ 关闭套接字1.2 UDP客户端流程① 建立UDP套接字② 发送信息给服务器③ 接收来自服务器的信息④ 关闭套接字1.3 UDP服务器、客户端的编程模型图2. 普通UDP服务器编程实现2.1 UDP服务器服务端代码基本框架服务端只有两个成员变量,端口号和自己的socket文件描述符,服务端不需要指定自己的IP地址,原因下文会做说明。class UdpServer { public: // 构造函数,创建一个服务端对象时需要显示传入一个端口号给服务端 UdpServer(const int port) :_port(port) ,_sockfd(-1) {} // 析构函数,当服务端对象销毁时关闭打开的socket文件描述符 ~UdpServer() { if(_sockfd >= 0) { close(_sockfd); } } private: int _port; // 服务端进程的端口号 int _sockfd;// 服务端进程的socket打开文件描述符 };初始化服务端创建socket文件描述符将服务端自己的套接字地址和刚刚创建的socket文件描述符绑定起来void InitServer() { // 1、创建socket文件描述符 if((_sockfd=socket(AF_INET, SOCK_DGRAM, 0)) == -1) { cerr<<"socket error"<<endl; return; } cout<<"socket sucess"<<endl; // 2、将服务端自己的套接字地址和刚刚创建的socket文件描述符绑定起来 struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(_port); local.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(_sockfd, (struct sockaddr*)&local, sizeof(local)) == -1) { cerr <<"bind error"<<endl; return; } cout<<"bind sucess"<<endl; }注意事项绑定操作我们是通过bind函数来完成的,该函数是把套接字地址和socket文件描述符给绑定联系起来。而套接字地址就包括端口号和IP地址,在这里我们可以把IP地址设为INADDR_ANY,表示该服务器可以收发本主机中所有网卡的数据。最后还要注意通过htons、htonl等整型数据字节序转化函数把P地址和端口号这些整型数据的字节序转化成网络字节序,然后再放到套接字地址变量中。启动服务端void Loop() { #define SIZE 128 // buffer用于接收客户端传来的数据 char buffer[SIZE]; // peer用于接收客户端的套接字地址信息 struct sockaddr_in peer; // len用来接收客户端套接字地址结构的大小 socklen_t len = sizeof(peer); while(true) { // 1、通过recvfrom函数不断接收客户端传来的数据和信息 ssize_t size = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len); // 2、解析客户端的数据和信息 if(size >= 0) { buffer[size] = '\0'; int port = ntohs(peer.sin_port); string ip = inet_ntoa(peer.sin_addr); cout<<'['<<ip<<' '<<port<<"]#"<<buffer<<endl; } else { cerr<<"recvfrom error"<<endl; } } }注意事项recvfrom函数不仅可以拿到客户端传来的数据,还可以拿到客户端的IP地址和端口号,服务端有了客户端的IP地址和端口号之后我们可以通过sendto函数再发送数据回去给客户端进程,实现客户端、服务端的双向网络通信。2.2 UDP客户端客户端代码基本框架客户端一般是要把任务传给服务器端,让服务端去处理任务的,所以客户端进程需要知道服务端的IP地址和端口号。class UdpClient { public: // 构造函数,需要显示传入目的服务端的IP地址和端口号,作为数据发送的目的地 UdpClient(const string& serverIp, const int serverPort) :_sockfd(-1) ,_serverIp(serverIp) ,_serverPort(serverPort) {} // 析构函数,关闭打开的socket文件描述符 ~UdpClient() { if(_sockfd >= 0) { close(_sockfd); } } private: int _sockfd; // 客户端进程的socket文件描述符 string _serverIp;// 服务端进程的IP地址 int _serverPort; // 服务端进程的端口号 };初始化客户端这里只需要创建客户端网络进程的socket文件描述符即可,而不需要将socket文件描述符和自己的套接字地址进行绑定。因为没人会关心你客户端的IP地址和端口号,而服务端作为服务的提供者,所有客户端进程都需要向服务端进程发送任务,即服务端的IP地址和端口号是要被众所周知的,所以服务端需要进行绑定操作而客户端不需要。void InitUdpClient() { if((_sockfd=socket(AF_INET, SOCK_DGRAM, 0)) == -1) { cerr<<"socket error"<<endl; return; } }启动客户端void Start() { string msg; // 在struct sockaddr_in结构中填入服务端的套接字地址 struct sockaddr_in peer; memset(&peer, 0, sizeof(peer)); peer.sin_family = AF_INET; peer.sin_port = htons(_serverPort); peer.sin_addr.s_addr = inet_addr(_serverIp.c_str()); // 通过sendto函数发送数据到服务端 while(true) { cout<<"Please Enter# "; getline(cin, msg); sendto(_sockfd, msg.c_str(), msg.size(), 0, (struct sockaddr*)&peer, sizeof(peer)); } }2.3 测试普通UDP服务器把上面UDP的服务端和客户端的实现代码分别写到udp_server.h 和 udp_client.h两个头文件中。udp_server.cpp创建一个服务端对象初始化服务端对象启动服务端对象#include "udp_server.h" // 运行可执行程序时,这里通过命令行参数传入服务端的端口号 int main(int argc, char** argv) { if(argc != 2) { cerr<<"Usage:"<<"./ServerName"<<" port"<<endl; return -1; } // 1、把第二个命令行参数转为整型,拿到端口号,用这个端口号去构造一个服务端对象 UdpServer* svr = new UdpServer(atoi(argv[1])); // 2、初始化服务端对象 svr->InitServer(); // 3、启动服务端对象 svr->Loop(); return 0; }udp_client.cpp创建一个客户端对象初始化客户端对象启动客户端对象#include "udp_client.h" // 运行可执行程序时,通过命令行参数传入目的服务端的IP地址和端口号 int main(int argc, char** argv) { if(argc != 3) { cout<<"Usage:"<<"./ServerName"<<" ServerIp ServerPort"<<endl; return -1; } // 1、解析命令行参数传入的IP地址和端口号 string serverIp = argv[1]; int serverPort = atoi(argv[2]); // 2、构造一个客户端对象 UdpClient* clt = new UdpClient(serverIp, serverPort); // 3、初始化客户端 clt->InitUdpClient(); // 4、启动客户端 clt->Start(); return 0; }结果测试编译生成可执行程序分别启动可执行程序,进行本地环回测试发现客户端发送的数据能被服务器接收到四. TCP套接字编程1. TCP套接字编程模型图TCP套接字编程经常使用在客户端/服务器编程模型(简称C/S模型)中,C/S模型根据复杂度,可分为简单的客户端/服务端模型和复杂的客户端/服务端模型。简单的客户端/服务端模型是一对一关系,即一个服务器端某一时间段内只对应处理一个客户端的请求,迭代服务器模型属于此模型。复杂的客户端/服务端模型是一对多关系,即一个服务器端某一时间段内对应处理多个客户端的请求,并发服务器模型属于此模型。迭代服务器模型和并发服务器模型是socket编程中最常使用的两种编程模型。迭代服务器模型和并发服务器模型的服务端处理流程如下图所示:下图是更加具体的TCP套接字编程模型图,此模型不仅适合迭代服务器,也适合并发服务器,两者实现的流程类似,只不过并发服务器接收客户请求(accept)后会用fork调用子进程,由子进程处理客户端的请求。2. TCP编程流程说明2.1 服务器编程流程①:创建套接字②:绑定套接字③:设置套接字为监听模式,进入被动接收连接状态④:接受请求,建立连接⑤:读写数据⑥:终止连接2.2 客户端编程流程①:创建套接字②:与远程服务器建立连接③:读写数据④:终止连接3. TCP网络数据读写说明在网络程序中,向套接字文件描述符写数据时有以下两种可能:write的返回值大于等于0,表示写了部分或者全部的数据,最后数值为实际写入的字节数(0表明什么都没写)。write的返回值等于-1,此时写出现错误,需要根据错误类型来处理。如果错误号为EINTR,则为中断引起的,可以忽略进行继续写操作;如果是其他错误号,表示网络连接出现了问题(可能对方关闭了连接),则需报错退出。与向套接字文件描述符写数据不同,读数据有三种可能:read的返回值大于0,表示读了部分或者全部的数据。read的返回值等于0,表示写端关闭了。read的返回值等于-1,表明读出现了错误,需要根据错误类型来处理,如果错误号为EINTR,则为中断引起的,可以忽略进行继续读操作;如果是其他错误号,表示网络连接出现了问题,则需要报错退出。4. 迭代服务器编程下面代码实现的是典型的迭代服务器,服务端的功能是接收客户发送来的字符串数据并原封不动地发回去。4.1 服务端代码tcp_server.h:用来存放服务端类的实现。#define BACK_LOG 10 #define BUFF_SIZE 1024 class TcpServer { public: TcpServer(const int port) :_port(port) ,_listenSock(-1) {} ~TcpServer() { if(_listenSock >= 0) { close(_listenSock); } } void InitServer() { // 1、创建套接字 _listenSock = socket(AF_INET, SOCK_STREAM, 0); // 2、绑定套接字 struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(_port); local.sin_addr.s_addr = INADDR_ANY; bind(_listenSock, (struct sockaddr*)&local, sizeof(local)); // 3、设置监听套接字 listen(_listenSock, BACK_LOG); } // 服务客户端 void Service(const int linkSock, const string& ip, const int port) { char buff[BUFF_SIZE]; while(1) { ssize_t size = read(linkSock, buff, sizeof(buff)-1); if(size > 0) { buff[size] = 0; cout<<'['<<ip<<':'<<port<<"]# "<<buff<<endl; write(linkSock, buff, size); } else if(size == 0) { cout<<"client close!"<<endl; break; } else { cerr<<"read error"<<endl; break; } } // 服务完成后要记得关闭该连接套接字 close(linkSock); } // 启动服务器 void LoopServer() { struct sockaddr_in peer; memset(&peer, 0, sizeof(peer)); socklen_t len = sizeof(peer); // 不断地监听获取客户端的连接请求 while(1) { int linkSock = accept(_listenSock, (struct sockaddr*)&peer, &len); // 若该套接字监听失败,继续监听下一个套接字即可 if(linkSock == -1) { cout<<"accept error, continue next link"<<endl; continue; } int port = ntohs(peer.sin_port); string ip = inet_ntoa(peer.sin_addr); cout<<"get a new link, sockfd is "<<linkSock<<endl; // 连接成功后,为客户端提供服务 Service(linkSock, ip, port); } } private: int _port; int _listenSock; };tcp_server.cpp:创建一个服务端对象,并初始化和启动它。#include "tcp_server.h" int main(int argc, char* argv[]) { if(argc != 2) { cout<<"Usage:./ServerProc Serverport"<<endl; exit(-1); } // 解析参数 int port = atoi(argv[1]); // 根据参数去创建一个服务端对象 TcpServer* svr = new TcpServer(port); // 初始化、启动务端 svr->InitServer(); svr->LoopServer(); // 最后delete服务端对象 delete svr; return 0; }4.2 客户端代码tcp_client.h:用来存放客户端类的实现class TcpClient { public: TcpClient(const string& serverIp, const int serverPort) :_serverIp(serverIp) ,_serverPort(serverPort) ,_linkSock(-1) {} ~TcpClient() { if(_linkSock >= 0) { close(_linkSock); } } // 初始化客户端 void InitClient() { // 初始化阶段只需创建套接字即可 _linkSock = socket(AF_INET, SOCK_STREAM, 0); } // 连接成功后,发送信息给服务端,然后在接收服务端返回的信息 void Request() { string msg; char echoBuff[1024]; while(1) { cout<<"please enter# "; getline(cin, msg); write(_linkSock, msg.c_str(), msg.size()); ssize_t size = read(_linkSock, echoBuff, sizeof(echoBuff)-1); if(size > 0) { echoBuff[size] = 0; cout<<"server echo# "<<echoBuff<<endl;; } else if(size == 0) { cout<<"server close!"<<endl; break; } else { cerr<<"read error"<<endl; break; } } } // 启动客户端,用已经创建出来的套接字去连接服务端并请求其处理任务 void Start() { struct sockaddr_in peer; memset(&peer, 0, sizeof(peer)); peer.sin_family = AF_INET; peer.sin_port = htons(_serverPort); peer.sin_addr.s_addr = inet_addr(_serverIp.c_str()); if(connect(_linkSock, (struct sockaddr*)&peer, sizeof(peer)) == -1) { cerr<<"connect error"<<endl; } else { cout<<"connect success"<<endl; Request(); } } private: string _serverIp; int _serverPort; int _linkSock; };tcp_client.cpp:创建一个客户端对象并初始化和启动它。#include "tcp_client.h" int main(int argc, char* argv[]) { if(argc != 3) { cout<<"Usage:./clientProc serverIp serverPort"<<endl; exit(-1); } // 解析参数 string ip = argv[1]; int port = atoi(argv[2]); // 根据参数去创建一个客户端对象 TcpClient* clt = new TcpClient(ip, port); // 初始化、启动客户端 clt->InitClient(); clt->Start(); // 最后delete客户端对象 delete clt; return 0; }4.3 测试迭代服务器两个会话分别启动服务端(左边)和客户端(右边),客户端发送数据给服务端,结果服务端能接收到数据并回响给客户端,说明该迭代服务器实现成功。迭代服务器存在明显的的缺点,即一个服务器端某一时间段内只对应处理一个客户端的请求,下图可以看到如果再另起一个客户端进程去连接服务器,因为上一个客户端的服务还没有完成所以新起的客户端进程并不能享受服务。5. 并发服务器编程5.1 并发服务器编程注意事项进程是一个程序的一次运行过程,它是一个动态实体,是独立的任务,它拥有独立的地址空间、执行堆栈、文件描述符等。每个进程拥有独立的地址空间,在进程不存在父子关系的情况下,互不影响。进程的终止存在两种可能:父进程先于子进程终止(由init进程领养),子进程先于主进程终止。对于后者,系统内核为子进程保留一定的状态信息(进程ID、终止状态、CPU时间等),并向其父进程发送SIGCHLD信号。当父进程调用wait或waitpid函数时,将获取这些信息,获取后内核将对僵尸进程进行清理。如果父进程设置了忽略SIGCHLD信号或对SIGCHLD信号提供了处理函数,即使不调用wait或waitpid函数,内核也会清理僵尸进程。父进程调用wait函数处理子进程退出信息时,会存在下面所述的问题。在有多个子进程的情况下,wait函数只等待最先到达的子进程的终止信息。比如下图中父进程有三个子进程,由于SIGCHLD信号不排队,在SIGCHLD信号同时到来后,父进程的wait函数只执行一次,这样将留下两个“僵尸进程”,使用waitpid函数并设置WNOHANG选项可以解决这个问题。综上所述,在多进程并发的情况下,防止子进程变成僵尸进程的常见方法有如下三种。①:父进程调用signal(SIGCHLD,SIG_IGN)对子进程退出信号进行忽略,或者把SIG_IGN替换为其他处理函数,设置对SIGCHLD信号的处理。②:父进程调用waitpid(-1, NULL, WNOHANG)对所有的子进程SIGCHLD信号进行处理。③:服务端进程先创建一个子进程(儿子进程),然后这个子进程再创建一个子进程(孙子进程),让孙子进程去处理任务并终止儿子进程,这样孙子进程处理完任务后因为没有父进程了,所以这个孙子进程会被init进程领养并释放。5.2 并发服务器文件描述符变化图下图画出了并发服务器文件描述符的变化流程图。其中listenfd为服务端的socket监听文件描述符,connfd为accept函数返回的socket连接文件描述符。服务器调用accept函数时,客户端与服务端文件描述符如图所示:服务器调用accept函数后,客户端与服务端文件描述符如图所示:服务端调用fork函数后,客户端与服务端文件描述符如下图所示:服务端父进程关闭连接套接字,子进程关闭监听套接字,客户端与服务端文件描述符状况如下图所示:PS:并发服务器fork后父进程一定要关闭子进程的连接套接字;而子进程要关闭父进程的监听套接字,以免误操作。5.3 TCP并发服务器代码实现并发服务器处理流程① 客户端首先发起链接。② 服务端进程accept打开一个新的连接套接字与客户端进行连接,accept在一个while(1)循环内等待客户端的连接。③ 服务端fork一个子进程,同时父进程close关闭子进程连接套接字,循环等待下一进程。④ 服务端子进程colse父进程监听套接字,并用连接套接字保持与客户端的连接,客户端发送数据到服务端进程,然后阻塞等待服务端返回。⑤ 子进程接收数据,进行业务处理,然后发送数据给客户端。⑥ 子进程关闭连接,然后退出。并发服务器服务端代码只需在迭代服务器的基础上修改服务端的启动部分代码即可,当服务端连接成功拿到新的连接套接字时,服务端进程fork创建子进程,让子进程去执行客户端发来的任务,注意服务端进程需要忽略对SIGCHLD信号的处理。先启动一个客户端1和一个服务端,发现确实能够正常通信:再启动一个客户端2,也能够正常和服务端通信:补充1:另一多进程版本的服务端编写服务端进程先创建一个子进程(儿子进程),然后这个子进程再创建一个子进程(孙子进程),让孙子进程去处理任务并终止儿子进程,这样孙子进程处理完任务后因为没有父进程了,所以这个孙子进程会被init进程领养并释放。只需修改服务端连接成功之后的那部分的代码即可:结果演示,一个服务端进程依然可以同时为多个客户端进程提供服务:补充2:多线程版本服务端编写创建进程的开销是要比创建线程大得多的,我们的主线程在连接成功后可以考虑去创建线程来处理任务,在编码时要注意以下几点:子线程和主线程共享一个打开文件描述符表。子线程执行函数如果要封装到服务端类中的话,需要给它的执行函数加上static关键字,以解除this指针对参数列表的影响。类的静态成员函数只能访问类的静态成员变量和成员函数,非静态的不能够访问到。涉及到的客户端类代码如下:class TcpServer { public: //...其他成员函数省略 // 多线程版本 // 启动服务器 void LoopServer() { struct sockaddr_in peer; memset(&peer, 0, sizeof(peer)); socklen_t len = sizeof(peer); // 不断地监听获取客户端的连接请求 while(1) { int linkSock = accept(_listenSock, (struct sockaddr*)&peer, &len); // 若该套接字监听失败,继续监听下一个套接字即可 if(linkSock == -1) { cout<<"accept error, continue next link"<<endl; continue; } int port = ntohs(peer.sin_port); string ip = inet_ntoa(peer.sin_addr); cout<<"get a new link, sockfd is "<<linkSock<<endl; // 建立连接后,创建子线程去处理任务 Param* pm = new Param(linkSock, ip, port); pthread_t tid; pthread_create(&tid, nullptr, Routine, pm); } } // 子线程处理任务函数(注意要设为静态的,要不然会参数里会有this指针) static void* Routine(void* arg) { pthread_detach(pthread_self()); Param* pm = (Param*)arg; Service(pm->_sockfd, pm->_ip, pm->_port); delete pm; return nullptr; } // 服务客户端(也要设为静态的,因为Routine的逻辑中有使用到该函数) static void Service(const int linkSock, const string& ip, const int port) { char buff[BUFF_SIZE]; while(1) { ssize_t size = read(linkSock, buff, sizeof(buff)-1); if(size > 0) { buff[size] = 0; cout<<'['<<ip<<':'<<port<<"]# "<<buff<<endl; write(linkSock, buff, size); } else if(size == 0) { cout<<"client close!"<<endl; break; } else { cerr<<"read error"<<endl; break; } } // 服务完成后要记得关闭该连接套接字 close(linkSock); } private: int _port; int _listenSock; };结果演示:不论主线程还是其创造出来的子线程,它们都共属同一个进程,每一个子线程要使用一个自己的连接套接字去处理任务,又因为它们共用同一张打开文件描述符表,所以各自分配到的套接字不同。
补充:来自oracle 9i的经典测试表表1:emp员工表员工表创建语句如下:DROP TABLE IF EXISTS `emp`; CREATE TABLE `emp` ( `empno` int(6) unsigned zerofill NOT NULL COMMENT '雇员编号', `ename` varchar(10) DEFAULT NULL COMMENT '雇员姓名', `job` varchar(9) DEFAULT NULL COMMENT '雇员职位', `mgr` int(4) unsigned zerofill DEFAULT NULL COMMENT '雇员领导编号', `hiredate` datetime DEFAULT NULL COMMENT '雇佣时间', `sal` decimal(7,2) DEFAULT NULL COMMENT '工资月薪', `comm` decimal(7,2) DEFAULT NULL COMMENT '奖金', `deptno` int(2) unsigned zerofill DEFAULT NULL COMMENT '部门编号' );接下来向员工表中插入一些数据:insert into emp (empno, ename, job, mgr, hiredate, sal, comm, deptno) values (7369, 'SMITH', 'CLERK', 7902, '1980-12-17', 800, null, 20); insert into emp (empno, ename, job, mgr, hiredate, sal, comm, deptno) values (7499, 'ALLEN', 'SALESMAN', 7698, '1981-02-20', 1600, 300, 30); insert into emp (empno, ename, job, mgr, hiredate, sal, comm, deptno) values (7521, 'WARD', 'SALESMAN', 7698, '1981-02-22', 1250, 500, 30); insert into emp (empno, ename, job, mgr, hiredate, sal, comm, deptno) values (7566, 'JONES', 'MANAGER', 7839, '1981-04-02', 2975, null, 20); insert into emp (empno, ename, job, mgr, hiredate, sal, comm, deptno) values (7654, 'MARTIN', 'SALESMAN', 7698, '1981-09-28', 1250, 1400, 30); insert into emp (empno, ename, job, mgr, hiredate, sal, comm, deptno) values (7698, 'BLAKE', 'MANAGER', 7839, '1981-05-01', 2850, null, 30); insert into emp (empno, ename, job, mgr, hiredate, sal, comm, deptno) values (7782, 'CLARK', 'MANAGER', 7839, '1981-06-09', 2450, null, 10); insert into emp (empno, ename, job, mgr, hiredate, sal, comm, deptno) values (7788, 'SCOTT', 'ANALYST', 7566, '1987-04-19', 3000, null, 20); insert into emp (empno, ename, job, mgr, hiredate, sal, comm, deptno) values (7839, 'KING', 'PRESIDENT', null, '1981-11-17', 5000, null, 10); insert into emp (empno, ename, job, mgr, hiredate, sal, comm, deptno) values (7844, 'TURNER', 'SALESMAN', 7698,'1981-09-08', 1500, 0, 30); insert into emp (empno, ename, job, mgr, hiredate, sal, comm, deptno) values (7876, 'ADAMS', 'CLERK', 7788, '1987-05-23', 1100, null, 20); insert into emp (empno, ename, job, mgr, hiredate, sal, comm, deptno) values (7900, 'JAMES', 'CLERK', 7698, '1981-12-03', 950, null, 30); insert into emp (empno, ename, job, mgr, hiredate, sal, comm, deptno) values (7902, 'FORD', 'ANALYST', 7566, '1981-12-03', 3000, null, 20); insert into emp (empno, ename, job, mgr, hiredate, sal, comm, deptno) values (7934, 'MILLER', 'CLERK', 7782, '1982-01-23', 1300, null, 10);结果显示:表2:dept部门表部门表创建语句如下:DROP TABLE IF EXISTS `dept`; CREATE TABLE `dept` ( `deptno` int(2) unsigned zerofill NOT NULL COMMENT '部门编号', `dname` varchar(14) DEFAULT NULL COMMENT '部门名称', `loc` varchar(13) DEFAULT NULL COMMENT '部门所在地点' );部门表数据插入:insert into dept (deptno, dname, loc) values (10, 'ACCOUNTING', 'NEW YORK'); insert into dept (deptno, dname, loc) values (20, 'RESEARCH', 'DALLAS'); insert into dept (deptno, dname, loc) values (30, 'SALES', 'CHICAGO'); insert into dept (deptno, dname, loc) values (40, 'OPERATIONS', 'BOSTON');结果显示:表3:salgrade工资等级表工资等级表创建语句如下:DROP TABLE IF EXISTS `salgrade`; CREATE TABLE `salgrade` ( `grade` int(11) DEFAULT NULL COMMENT '等级', `losal` int(11) DEFAULT NULL COMMENT '此等级最低工资', `hisal` int(11) DEFAULT NULL COMMENT '此等级最高工资' );数据插入:insert into salgrade (grade, losal, hisal) values (1, 700, 1200); insert into salgrade (grade, losal, hisal) values (2, 1201, 1400); insert into salgrade (grade, losal, hisal) values (3, 1401, 2000); insert into salgrade (grade, losal, hisal) values (4, 2001, 3000); insert into salgrade (grade, losal, hisal) values (5, 3001, 9999);结果显示:一. 聚合函数1. 函数介绍聚合函数又叫组函数,通常是对表中的数据进行统计和计算,一般结合分组(group by)来使用,用于统计和计算分组数据。函数特点每个组函数只能接收一个参数(字段名或者表达式) 统计结果中默认忽略字段为NULL的记录,要想列值为NULL的行也参与组函数的计算,必须使用IFNULL函数对NULL值做转换。不允许嵌套使用组函数,比如sum(max(xx))。可以在参数前加上DISTINCT先进行数据去重,然后在执行函数。函数概览函数 说明COUNT([DISTINCT] expr) 返回查询到的数据的 数量SUM([DISTINCT] expr) 返回查询到的数据的 总和,不是数字没有意义AVG([DISTINCT] expr) 返回查询到的数据的 平均值,不是数字没有意义MAX([DISTINCT] expr) 返回查询到的数据的 最大值,不是数字没有意义MIN([DISTINCT] expr) 返回查询到的数据的 最小值,不是数字没有意义2. 使用举例为了方便举例,在这里新建一张学生成绩表,它包括如下5个字段:id:学生的学号,设为自增长主键。name:学生姓名,不允许为空。chinese:语文成绩,可以为空,默认0.0分。math:数学成绩,可以为空,默认0.0分。english:英语成绩,可以为空,默认0.0分。mysql> create table if not exists TestScores( -> id int unsigned primary key auto_increment, -> name varchar(20) not null, -> chinese float default 0.0, -> math float default 0.0, -> english float default 0.0 -> ); Query OK, 0 rows affected (0.02 sec) mysql> desc TestScores; +---------+------------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------+------------------+------+-----+---------+----------------+ | id | int(10) unsigned | NO | PRI | NULL | auto_increment | | name | varchar(20) | NO | | NULL | | | chinese | float | YES | | 0 | | | math | float | YES | | 0 | | | english | float | YES | | 0 | | +---------+------------------+------+-----+---------+----------------+ 5 rows in set (0.00 sec)接下来向该表中插入一些数据:mysql> insert into TestScores(name, chinese, math, english) values -> ('曹操', 67, 98, 56), -> ('孙权', 87, 78, 77), -> ('孙策', 88, 98, 90), -> ('刘备', 82, 84, 67), -> ('程咬金', 55, 85, 45), -> ('孙尚香', 70, 73, 78), -> ('诸葛亮', 75, 65, 30); Query OK, 7 rows affected (0.01 sec) Records: 7 Duplicates: 0 Warnings: 01、统计班级共有多少同学使用 * 做统计:mysql> select * from TestScores; +----+-----------+---------+------+---------+ | id | name | chinese | math | english | +----+-----------+---------+------+---------+ | 1 | 曹操 | 67 | 98 | 56 | | 2 | 孙权 | 87 | 78 | 77 | | 3 | 孙策 | 88 | 98 | 90 | | 4 | 刘备 | 82 | 84 | 67 | | 5 | 程咬金 | 55 | 85 | 45 | | 6 | 孙尚香 | 70 | 73 | 78 | | 7 | 诸葛亮 | 75 | 65 | 30 | +----+-----------+---------+------+---------+ 7 rows in set (0.00 sec) mysql> select count(*) from TestScores; +----------+ | count(*) | +----------+ | 7 | +----------+ 1 row in set (0.00 sec)当然我们也可以把统计出来的结果重命名:mysql> select count(*) as count_num from TestScores; +-----------+ | count_num | +-----------+ | 7 | +-----------+ 1 row in set (0.00 sec)2、统计本次考试的数学成绩分数个数以math字段为参数做统计,如果有NULL的话不会计入结果:mysql> select * from TestScores; +----+-----------+---------+------+---------+ | id | name | chinese | math | english | +----+-----------+---------+------+---------+ | 1 | 曹操 | 67 | 98 | 56 | | 2 | 孙权 | 87 | 78 | 77 | | 3 | 孙策 | 88 | 98 | 90 | | 4 | 刘备 | 82 | 84 | 67 | | 5 | 程咬金 | 55 | 85 | 45 | | 6 | 孙尚香 | 70 | 73 | 78 | | 7 | 诸葛亮 | 75 | 65 | 30 | +----+-----------+---------+------+---------+ 7 rows in set (0.00 sec) mysql> select count(math) from TestScores; +-------------+ | count(math) | +-------------+ | 7 | +-------------+ 1 row in set (0.00 sec)此外,我们还可以使用DISTINCT先去重再统计,得到的是去重后的成绩数量:mysql> select count(distinct math) from TestScores; +----------------------+ | count(distinct math) | +----------------------+ | 6 | +----------------------+ 1 row in set (0.00 sec)3、统计数学成绩总分使用sum聚合函数来计算数据的和。mysql> select sum(math) from TestScores; +-----------+ | sum(math) | +-----------+ | 581 | +-----------+ 1 row in set (0.00 sec)4、统计总平均分使用avg聚合函数来计算平均值。mysql> select avg(chinese+math+english) from TestScores; +---------------------------+ | avg(chinese+math+english) | +---------------------------+ | 221.14285714285714 | +---------------------------+ 1 row in set (0.00 sec)5、返回英语最高分使用max聚合函数来统计数据中的最大值。mysql> select max(english) from TestScores; +--------------+ | max(english) | +--------------+ | 90 | +--------------+ 1 row in set (0.00 sec)PS:对应的还有个min聚合函数用来统计数据中的最小值。3. group by子句的使用在select中使用group by子句可以对指定列进行分组查询,聚合函数常常需要结合group by子句一起使用。举例:使用文章最开始的oracle 9i经典测试表1、显示每个部门的平均工资和最高工资// 1、查看emp表的数据 mysql> select * from emp; +--------+--------+-----------+------+---------------------+---------+---------+--------+ | empno | ename | job | mgr | hiredate | sal | comm | deptno | +--------+--------+-----------+------+---------------------+---------+---------+--------+ | 007369 | SMITH | CLERK | 7902 | 1980-12-17 00:00:00 | 800.00 | NULL | 20 | | 007499 | ALLEN | SALESMAN | 7698 | 1981-02-20 00:00:00 | 1600.00 | 300.00 | 30 | | 007521 | WARD | SALESMAN | 7698 | 1981-02-22 00:00:00 | 1250.00 | 500.00 | 30 | | 007566 | JONES | MANAGER | 7839 | 1981-04-02 00:00:00 | 2975.00 | NULL | 20 | | 007654 | MARTIN | SALESMAN | 7698 | 1981-09-28 00:00:00 | 1250.00 | 1400.00 | 30 | | 007698 | BLAKE | MANAGER | 7839 | 1981-05-01 00:00:00 | 2850.00 | NULL | 30 | | 007782 | CLARK | MANAGER | 7839 | 1981-06-09 00:00:00 | 2450.00 | NULL | 10 | | 007788 | SCOTT | ANALYST | 7566 | 1987-04-19 00:00:00 | 3000.00 | NULL | 20 | | 007839 | KING | PRESIDENT | NULL | 1981-11-17 00:00:00 | 5000.00 | NULL | 10 | | 007844 | TURNER | SALESMAN | 7698 | 1981-09-08 00:00:00 | 1500.00 | 0.00 | 30 | | 007876 | ADAMS | CLERK | 7788 | 1987-05-23 00:00:00 | 1100.00 | NULL | 20 | | 007900 | JAMES | CLERK | 7698 | 1981-12-03 00:00:00 | 950.00 | NULL | 30 | | 007902 | FORD | ANALYST | 7566 | 1981-12-03 00:00:00 | 3000.00 | NULL | 20 | | 007934 | MILLER | CLERK | 7782 | 1982-01-23 00:00:00 | 1300.00 | NULL | 10 | +--------+--------+-----------+------+---------------------+---------+---------+--------+ 14 rows in set (0.00 sec) // 2、按照部门号对所有员工分组,然后以组为单位计算组内所有员工的平均工资和最高工资 mysql> select deptno, avg(sal), max(sal) from emp group by deptno; +--------+-------------+----------+ | deptno | avg(sal) | max(sal) | +--------+-------------+----------+ | 10 | 2916.666667 | 5000.00 | | 20 | 2175.000000 | 3000.00 | | 30 | 1566.666667 | 2850.00 | +--------+-------------+----------+ 3 rows in set (0.01 sec)PS:这里必须使用group by字句进行分组然后才能计算,因为我们最开始还查询了部门号deptno。// error:没有分组的话聚合函数默认统计所有员工的数据,但是这里select又查询了部门号deptno // 这样同时显示出来会有歧义,所以要使用group by进行分组 mysql> select deptno, avg(sal), max(sal) from emp; ERROR 1140 (42000): In aggregated query without GROUP BY, expression #1 of SELECT list contains nonaggregated column 'scott.emp.deptno'; this is incompatible with sql_mode=only_full_group_by // 不查询部门号deptno的话就没什么问题,这时统计的是所有员工的数据 mysql> select avg(sal), max(sal) from emp; +-------------+----------+ | avg(sal) | max(sal) | +-------------+----------+ | 2073.214286 | 5000.00 | +-------------+----------+ 1 row in set (0.00 sec)2、显示每个部门的每种岗位的平均工资和最低工资使用group by先对部门分组然后内部进行岗位分组,最后使用聚合函数计算结果:// 1、查询emp中表的数据 mysql> select * from emp; +--------+--------+-----------+------+---------------------+---------+---------+--------+ | empno | ename | job | mgr | hiredate | sal | comm | deptno | +--------+--------+-----------+------+---------------------+---------+---------+--------+ | 007369 | SMITH | CLERK | 7902 | 1980-12-17 00:00:00 | 800.00 | NULL | 20 | | 007499 | ALLEN | SALESMAN | 7698 | 1981-02-20 00:00:00 | 1600.00 | 300.00 | 30 | | 007521 | WARD | SALESMAN | 7698 | 1981-02-22 00:00:00 | 1250.00 | 500.00 | 30 | | 007566 | JONES | MANAGER | 7839 | 1981-04-02 00:00:00 | 2975.00 | NULL | 20 | | 007654 | MARTIN | SALESMAN | 7698 | 1981-09-28 00:00:00 | 1250.00 | 1400.00 | 30 | | 007698 | BLAKE | MANAGER | 7839 | 1981-05-01 00:00:00 | 2850.00 | NULL | 30 | | 007782 | CLARK | MANAGER | 7839 | 1981-06-09 00:00:00 | 2450.00 | NULL | 10 | | 007788 | SCOTT | ANALYST | 7566 | 1987-04-19 00:00:00 | 3000.00 | NULL | 20 | | 007839 | KING | PRESIDENT | NULL | 1981-11-17 00:00:00 | 5000.00 | NULL | 10 | | 007844 | TURNER | SALESMAN | 7698 | 1981-09-08 00:00:00 | 1500.00 | 0.00 | 30 | | 007876 | ADAMS | CLERK | 7788 | 1987-05-23 00:00:00 | 1100.00 | NULL | 20 | | 007900 | JAMES | CLERK | 7698 | 1981-12-03 00:00:00 | 950.00 | NULL | 30 | | 007902 | FORD | ANALYST | 7566 | 1981-12-03 00:00:00 | 3000.00 | NULL | 20 | | 007934 | MILLER | CLERK | 7782 | 1982-01-23 00:00:00 | 1300.00 | NULL | 10 | +--------+--------+-----------+------+---------------------+---------+---------+--------+ 14 rows in set (0.00 sec) // 2、group by后面跟需要分组的字段,有先后顺序 mysql> select avg(sal) 平均工资, min(sal) 最低工资, job, deptno from emp group by deptno, job; +--------------+--------------+-----------+--------+ | 平均工资 | 最低工资 | job | deptno | +--------------+--------------+-----------+--------+ | 1300.000000 | 1300.00 | CLERK | 10 | | 2450.000000 | 2450.00 | MANAGER | 10 | | 5000.000000 | 5000.00 | PRESIDENT | 10 | | 3000.000000 | 3000.00 | ANALYST | 20 | | 950.000000 | 800.00 | CLERK | 20 | | 2975.000000 | 2975.00 | MANAGER | 20 | | 950.000000 | 950.00 | CLERK | 30 | | 2850.000000 | 2850.00 | MANAGER | 30 | | 1400.000000 | 1250.00 | SALESMAN | 30 | +--------------+--------------+-----------+--------+ 9 rows in set (0.00 sec)PS:在select后面的列名称(除聚合函数外),如果后续我们要进行group by分组,那么凡是在select中出现的原表中的列名称,也必须在group by中出现。// error:查询了select字段,但是没有以它进行分组 mysql> select avg(sal) 平均工资, min(sal) 最低工资, job, deptno from emp group by deptno; ERROR 1055 (42000): Expression #3 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'scott.emp.job' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by // succeed:使用了聚合函数,查询的字段必须都分组 mysql> select avg(sal) 平均工资, min(sal) 最低工资, job, deptno from emp group by deptno, job; +--------------+--------------+-----------+--------+ | 平均工资 | 最低工资 | job | deptno | +--------------+--------------+-----------+--------+ | 1300.000000 | 1300.00 | CLERK | 10 | | 2450.000000 | 2450.00 | MANAGER | 10 | | 5000.000000 | 5000.00 | PRESIDENT | 10 | | 3000.000000 | 3000.00 | ANALYST | 20 | | 950.000000 | 800.00 | CLERK | 20 | | 2975.000000 | 2975.00 | MANAGER | 20 | | 950.000000 | 950.00 | CLERK | 30 | | 2850.000000 | 2850.00 | MANAGER | 30 | | 1400.000000 | 1250.00 | SALESMAN | 30 | +--------------+--------------+-----------+--------+ 9 rows in set (0.00 sec)3、显示平均工资低于2000的部门和它的平均工资// 1、以部门为单位分组,统计每个部门的平均工资 mysql> select avg(sal), deptno from emp group by deptno; +-------------+--------+ | avg(sal) | deptno | +-------------+--------+ | 2916.666667 | 10 | | 2175.000000 | 20 | | 1566.666667 | 30 | +-------------+--------+ 3 rows in set (0.00 sec) // error:分组后的数据不能使用where进行筛选 mysql> select avg(sal), deptno from emp group by deptno where avg(sal)<2000; ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'where avg(sal)<2000' at line 1 // succeed:分组后的数据只能用having字句进行筛选 mysql> select avg(sal), deptno from emp group by deptno having avg(sal)<2000; +-------------+--------+ | avg(sal) | deptno | +-------------+--------+ | 1566.666667 | 30 | +-------------+--------+ 1 row in set (0.00 sec)知识点补充where适用于原表数据的筛选;having适用于对分组后的表进行筛选。select时各种SQL字句的执行顺序:4. 聚合函数使用注意事项聚合函数是做统计用的,必须严格匹配格式:select 聚合函数(字段) from 表名使用,而不能单独作为表达式去使用,除非有进行分组。举例1:显示工资最高的员工的名字和工作岗位// 错误用法,聚合函数不能单独作为表达式去使用 mysql> select ename, max(sal) from emp; ERROR 1140 (42000): In aggregated query without GROUP BY, expression #1 of SELECT list contains nonaggregated column 'scott.emp.ename'; this is incompatible with sql_mode=only_full_group_by // 错误用法: mysql> select ename, sal from emp where sal=max(sal); ERROR 1111 (HY000): Invalid use of group function // 正确用法 mysql> select ename, sal from emp where sal=(select max(sal) from emp); +-------+---------+ | ename | sal | +-------+---------+ | KING | 5000.00 | +-------+---------+ 1 row in set (0.02 sec)举例二:显示每个部门的平均工资和最高工资mysql> select deptno, avg(sal), max(sal) from emp group by deptno; +--------+-------------+----------+ | deptno | avg(sal) | max(sal) | +--------+-------------+----------+ | 10 | 2916.666667 | 5000.00 | | 20 | 2175.000000 | 3000.00 | | 30 | 1566.666667 | 2850.00 | +--------+-------------+----------+ 3 rows in set (0.01 sec)二. 日期函数1. 函数概览函数名称 描述current_date() 当前日期current_time() 当前时间current_timestamp 当前日期时间date_add(data, interval, d_value_type) 在date中添加日期或时间interval后的数值单位可以是:year day minute seconddate_sub(date, interval d_value_type) 在date中减去日期或时间interval后的数值单位可以是:year day minute seconddatediff(date1, date2) 两个日期的差,单位是天now() 当前日期时间date(datetime) 只显示日期部分time 只显示时间部分2. 使用举例1、获得当前日期// 使用函数current_date()函数 mysql> select current_date(); +----------------+ | current_date() | +----------------+ | 2022-06-30 | +----------------+ 1 row in set (0.00 sec)2、获得当前时间// 使用current_time()函数 mysql> select current_time(); +----------------+ | current_time() | +----------------+ | 12:57:46 | +----------------+ 1 row in set (0.00 sec)3、获得当前时间日期// 方法一:使用current_timestamp()函数 mysql> select current_timestamp(); +---------------------+ | current_timestamp() | +---------------------+ | 2022-06-30 12:58:54 | +---------------------+ 1 row in set (0.00 sec) // 方法二:使用now()函数 mysql> select now(); +---------------------+ | now() | +---------------------+ | 2022-06-30 12:59:48 | +---------------------+ 1 row in set (0.00 sec)4、date_add()的使用// 正整数的话就执行加操作 mysql> select date_add('2001-07-10', interval 3 day); +----------------------------------------+ | date_add('2001-07-10', interval 3 day) | +----------------------------------------+ | 2001-07-13 | +----------------------------------------+ 1 row in set (0.02 sec) // 负整数的话就是执行减操作 mysql> select date_add('2001-07-10', interval -3 day); +-----------------------------------------+ | date_add('2001-07-10', interval -3 day) | +-----------------------------------------+ | 2001-07-07 | +-----------------------------------------+ 1 row in set (0.00 sec) // interval后面数据的单位可以是year day minute second mysql> select date_add('2001-07-10', interval 3 second); +-------------------------------------------+ | date_add('2001-07-10', interval 3 second) | +-------------------------------------------+ | 2001-07-10 00:00:03 | +-------------------------------------------+ 1 row in set (0.00 sec)5、date_sub()的使用mysql> select date_sub('2001-07-10', interval 3 day); +----------------------------------------+ | date_sub('2001-07-10', interval 3 day) | +----------------------------------------+ | 2001-07-07 | +----------------------------------------+ 1 row in set (0.00 sec) mysql> select date_sub('2001-07-10', interval -3 day); +-----------------------------------------+ | date_sub('2001-07-10', interval -3 day) | +-----------------------------------------+ | 2001-07-13 | +-----------------------------------------+ 1 row in set (0.00 sec) mysql> select date_sub('2001-07-10', interval 3 second); +-------------------------------------------+ | date_sub('2001-07-10', interval 3 second) | +-------------------------------------------+ | 2001-07-09 23:59:57 | +-------------------------------------------+ 1 row in set (0.00 sec)6、计算两个日期之间相差多少天// 左边的日期 - 右边的日期 mysql> select datediff('2001-07-29', '2001-07-10'); +--------------------------------------+ | datediff('2001-07-29', '2001-07-10') | +--------------------------------------+ | 19 | +--------------------------------------+ 1 row in set (0.00 sec)7、只显示日期或只显示时间// 1、date函数只显示日期部分,如果参数没有日期就显示NULL mysql> select date('2001-07-09 12:59:48'); +-----------------------------+ | date('2001-07-09 12:59:48') | +-----------------------------+ | 2001-07-09 | +-----------------------------+ 1 row in set (0.00 sec) mysql> select date('12:59:48'); +------------------+ | date('12:59:48') | +------------------+ | NULL | +------------------+ 1 row in set, 1 warning (0.00 sec) // 2、time函数只显示时间部分,如果没有时间的话显示的数据不确定(可能为空,也能为其他时间) mysql> select time('2001-07-09 12:59:48'); +-----------------------------+ | time('2001-07-09 12:59:48') | +-----------------------------+ | 12:59:48 | +-----------------------------+ 1 row in set (0.00 sec)8、设计一个留言表表的内容包括以下三个字段:id:用户的id,设为自增长主键。 content:用户的留言内容,不能为空。 sendtime:留言日期时间,可以为空,默认是当前的。 mysql> create table if no exists msg( -> id int primary key auto_increment comment '用户id', -> content varchar(30) not null comment '留言内容', -> sendtime datetime default now() comment '留言日期时间' -> ); Query OK, 0 rows affected (0.03 sec)向留言表中间隔一分钟插入一条数据:mysql> insert into msg (content) values ('张三我爱你!!!'); Query OK, 1 row affected (0.00 sec) mysql> insert into msg (content) values ('祝李四生日快乐'); Query OK, 1 row affected (0.01 sec) mysql> insert into msg (content) values ('祝妈妈母亲节快乐'); Query OK, 1 row affected (0.00 sec) mysql> insert into msg (content) values ('熊猫太可爱了'); Query OK, 1 row affected (0.01 sec) mysql> select * from msg; +----+--------------------------+---------------------+ | id | content | sendtime | +----+--------------------------+---------------------+ | 1 | 张三我爱你!!! | 2022-06-30 13:50:48 | | 2 | 祝李四生日快乐 | 2022-06-30 13:51:32 | | 3 | 祝妈妈母亲节快乐 | 2022-06-30 13:52:55 | | 4 | 熊猫太可爱了 | 2022-06-30 13:53:46 | +----+--------------------------+---------------------+ 4 rows in set (0.00 sec)显示所有留言信息,发布日期只显示日期,不用显示时间mysql> select content, date(sendtime) from msg; +--------------------------+----------------+ | content | date(sendtime) | +--------------------------+----------------+ | 张三我爱你!!! | 2022-06-30 | | 祝李四生日快乐 | 2022-06-30 | | 祝妈妈母亲节快乐 | 2022-06-30 | | 熊猫太可爱了 | 2022-06-30 | +--------------------------+----------------+ 4 rows in set (0.00 sec)查询在2分钟内发布的帖子// 1、查询留言板的数据 mysql> select * from msg; +----+--------------------------+---------------------+ | id | content | sendtime | +----+--------------------------+---------------------+ | 1 | 张三我爱你!!! | 2022-06-30 13:50:48 | | 2 | 祝李四生日快乐 | 2022-06-30 13:51:32 | | 3 | 祝妈妈母亲节快乐 | 2022-06-30 13:52:55 | | 4 | 熊猫太可爱了 | 2022-06-30 13:53:46 | +----+--------------------------+---------------------+ 4 rows in set (0.00 sec) // 2、显示当前时间 mysql> select now(); +---------------------+ | now() | +---------------------+ | 2022-06-30 13:54:26 | +---------------------+ 1 row in set (0.00 sec) // 3、查询在2分钟内发布的帖子 mysql> select * from msg where date_add(sendtime, interval 2 minute) > now(); +----+--------------------------+---------------------+ | id | content | sendtime | +----+--------------------------+---------------------+ | 3 | 祝妈妈母亲节快乐 | 2022-06-30 13:52:55 | | 4 | 熊猫太可爱了 | 2022-06-30 13:53:46 | +----+--------------------------+---------------------+ 4 rows in set (0.00 sec)三. 字符串函数1. 函数概览函数名称 描述charset(str) 返回字符串的字符集类型concat(string [, …]) 拼接字符串instr(string, substring) 返回substring在string中出现的位置,没有返回0ucase(string) 把string中的字符都转换成大写lcase(string) 把string中的字符都转换成小写left(string, length) 从string中的左边起取length个字符length(string) 返回string所占的空间大小,单位是字节replace(str, search_str, replace_str) 在str中用replace_str替换search_strstrcmp(string1, string2) 逐字符比较两字符串大小substring(str, position [, length]) 从str的position开始,取length个字符ltrim(string) rtrim(string) trim(string) 去除前空格或后空格2. 使用举例下面使用oracle 9i的emp表举例,该表数据如下:获取emp表中ename字段的字符集mysql> select ename, charset(ename) from emp; +--------+----------------+ | ename | charset(ename) | +--------+----------------+ | SMITH | utf8 | | ALLEN | utf8 | | WARD | utf8 | | JONES | utf8 | | MARTIN | utf8 | | BLAKE | utf8 | | CLARK | utf8 | | SCOTT | utf8 | | KING | utf8 | | TURNER | utf8 | | ADAMS | utf8 | | JAMES | utf8 | | FORD | utf8 | | MILLER | utf8 | +--------+----------------+ 14 rows in set (0.00 sec)要求显示emp表中的信息,显示格式:“XXX的工号是XXX,岗位是XXX”mysql> select concat(ename, '的工号是', empno, ',岗位是', job) from emp; +-----------------------------------------------------------+ | concat(ename, '的工号是', empno, ',岗位是', job) | +-----------------------------------------------------------+ | SMITH的工号是007369,岗位是CLERK | | ALLEN的工号是007499,岗位是SALESMAN | | WARD的工号是007521,岗位是SALESMAN | | JONES的工号是007566,岗位是MANAGER | | MARTIN的工号是007654,岗位是SALESMAN | | BLAKE的工号是007698,岗位是MANAGER | | CLARK的工号是007782,岗位是MANAGER | | SCOTT的工号是007788,岗位是ANALYST | | KING的工号是007839,岗位是PRESIDENT | | TURNER的工号是007844,岗位是SALESMAN | | ADAMS的工号是007876,岗位是CLERK | | JAMES的工号是007900,岗位是CLERK | | FORD的工号是007902,岗位是ANALYST | | MILLER的工号是007934,岗位是CLERK | +-----------------------------------------------------------+ 14 rows in set (0.00 sec)求emp表中员工姓名占用的字节数mysql> select ename, length(ename) from emp; +--------+---------------+ | ename | length(ename) | +--------+---------------+ | SMITH | 5 | | ALLEN | 5 | | WARD | 4 | | JONES | 5 | | MARTIN | 6 | | BLAKE | 5 | | CLARK | 5 | | SCOTT | 5 | | KING | 4 | | TURNER | 6 | | ADAMS | 5 | | JAMES | 5 | | FORD | 4 | | MILLER | 6 | +--------+---------------+ 14 rows in set (0.00 sec)将EMP表中所有员工名字中带有S的替换成’上海’mysql> select ename, replace(ename, 'S', '上海') from emp; +--------+-------------------------------+ | ename | replace(ename, 'S', '上海') | +--------+-------------------------------+ | SMITH | 上海MITH | | ALLEN | ALLEN | | WARD | WARD | | JONES | JONE上海 | | MARTIN | MARTIN | | BLAKE | BLAKE | | CLARK | CLARK | | SCOTT | 上海COTT | | KING | KING | | TURNER | TURNER | | ADAMS | ADAM上海 | | JAMES | JAME上海 | | FORD | FORD | | MILLER | MILLER | +--------+-------------------------------+ 14 rows in set (0.00 sec)截取EMP表中ename字段的第二个到第三个字符mysql> select ename, substring(ename, 2, 2) from emp; +--------+------------------------+ | ename | substring(ename, 2, 2) | +--------+------------------------+ | SMITH | MI | | ALLEN | LL | | WARD | AR | | JONES | ON | | MARTIN | AR | | BLAKE | LA | | CLARK | LA | | SCOTT | CO | | KING | IN | | TURNER | UR | | ADAMS | DA | | JAMES | AM | | FORD | OR | | MILLER | IL | +--------+------------------------+ 14 rows in set (0.00 sec)以首字母小写的方式显示所有员工的姓名mysql> select concat(lcase(substring(ename, 1, 1)), substring(ename, 2)) from emp; +------------------------------------------------------------+ | concat(lcase(substring(ename, 1, 1)), substring(ename, 2)) | +------------------------------------------------------------+ | sMITH | | aLLEN | | wARD | | jONES | | mARTIN | | bLAKE | | cLARK | | sCOTT | | kING | | tURNER | | aDAMS | | jAMES | | fORD | | mILLER | +------------------------------------------------------------+ 14 rows in set (0.00 sec)四. 数学函数1. 函数概览函数名称 描述abs(number) 绝对值函数bin(decimal_number) 把十进制数字转换成二进制hex(decimal_number) 把十进制数字转换成十六进制conv(number, from_base, to_base) 指定进制转换ceiling(number) 向上取整floor(number) 向下取整format(number, decimal_places) 四舍五入,decimal_places表示保留的小数位数rand() 返回随机浮点数,范围[0.0, 1.0]mod(number, denominator) 取模,求余2. 使用举例计算绝对值mysql> select abs(-3.14); +------------+ | abs(-3.14) | +------------+ | 3.14 | +------------+ 1 row in set (0.00 sec)向上取整(往大的数方向上取整)mysql> select ceiling(3.14), ceiling(-3.14), ceiling(3); +---------------+----------------+------------+ | ceiling(3.14) | ceiling(-3.14) | ceiling(3) | +---------------+----------------+------------+ | 4 | -3 | 3 | +---------------+----------------+------------+ 1 row in set (0.00 sec)向下取整(往小的数方向上取整)mysql> select floor(3.14), floor(-3.14), floor(3); +-------------+--------------+----------+ | floor(3.14) | floor(-3.14) | floor(3) | +-------------+--------------+----------+ | 3 | -4 | 3 | +-------------+--------------+----------+ 1 row in set (0.00 sec)四舍五入(需要指定小数位数)mysql> select format(3.5, 0), format(-3.5, 0), format(3.4, 0), format(-3.4, 0); +----------------+-----------------+----------------+-----------------+ | format(3.5, 0) | format(-3.5, 0) | format(3.4, 0) | format(-3.4, 0) | +----------------+-----------------+----------------+-----------------+ | 4 | -4 | 3 | -3 | +----------------+-----------------+----------------+-----------------+ 1 row in set (0.00 sec)产生1~100之间的随机数// 因为rand()函数得到的是随机浮点数,范围[0.0, 1.0] // rnad()再乘上100后向下取整得到范围[0, 99] // 最后再加上1得到范围[1, 100] mysql> select floor(rand()*100)+1; +---------------------+ | floor(rand()*100)+1 | +---------------------+ | 95 | +---------------------+ 1 row in set (0.00 sec) mysql> select floor(rand()*100)+1; +---------------------+ | floor(rand()*100)+1 | +---------------------+ | 24 | +---------------------+ 1 row in set (0.00 sec) mysql> select floor(rand()*100)+1; +---------------------+ | floor(rand()*100)+1 | +---------------------+ | 34 | +---------------------+ 1 row in set (0.00 sec)十进制数字转为二进制和十六进制// 转为二进制 mysql> select bin(10); +---------+ | bin(10) | +---------+ | 1010 | +---------+ 1 row in set (0.00 sec) // 转为十六进制 mysql> select hex(10); +---------+ | hex(10) | +---------+ | A | +---------+ 1 row in set (0.00 sec)执行取模运算// 发现取模后结果的正负号取决于被除数 mysql> select mod(10, 3); +------------+ | mod(10, 3) | +------------+ | 1 | +------------+ 1 row in set (0.00 sec) mysql> select mod(-10, -3); +--------------+ | mod(-10, -3) | +--------------+ | -1 | +--------------+ 1 row in set (0.00 sec) mysql> select mod(-10, 3); +-------------+ | mod(-10, 3) | +-------------+ | -1 | +-------------+ 1 row in set (0.00 sec) mysql> select mod(10, -3); +-------------+ | mod(10, -3) | +-------------+ | 1 | +-------------+ 1 row in set (0.00 sec)五. 其它函数1、user() 查询当前用户mysql> select user(); +----------------+ | user() | +----------------+ | root@localhost | +----------------+ 1 row in set (0.00 sec)2、database()显示当前正在使用的数据库// 1、查看当前正在使用的数据库 mysql> select database(); +------------+ | database() | +------------+ | ForTest | +------------+ 1 row in set (0.00 sec) // 2、切换数据库 mysql> use scott; Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A Database changed // 3、重新查看当前正在使用的数据库 mysql> select database(); +------------+ | database() | +------------+ | scott | +------------+ 1 row in set (0.00 sec)3、md5(str)对一个字符串进行md5摘要,摘要后得到一个32位字符串// 不论字符串长度有多长,摘要后得到的都是一个32位字符串 mysql> select md5('5201314'); +----------------------------------+ | md5('5201314') | +----------------------------------+ | 723d505516e0c197e42a6be3c0af910e | +----------------------------------+ 1 row in set (0.02 sec) mysql> select md5('admin'); +----------------------------------+ | md5('admin') | +----------------------------------+ | 21232f297a57a5a743894a0e4a801fc3 | +----------------------------------+ 1 row in set (0.00 sec)4、ifnull(val1, val2)如果val1不为null,返回val1,否则返回val2的值PS:val可以是任意类型的数据。// ifnull的作用类似于C/C++中的三目运算符 mysql> select ifnull('abc', '123'); +----------------------+ | ifnull('abc', '123') | +----------------------+ | abc | +----------------------+ 1 row in set (0.00 sec) mysql> select ifnull(null, '123'); +---------------------+ | ifnull(null, '123') | +---------------------+ | 123 | +---------------------+ 1 row in set (0.00 sec)
一. Linux下的用户我们购买服务器后最开始都是以root为用户名登陆的,这个root用户我们称为超级用户,他可以再linux系统下做任何事情,不受限制。除了超级用户root外我们还可以自己创建普通用户。1. 普通用户的创建首先要保证在root用户下(因为root权限最大)操作输入 adduser 用户名 创建用户输入 passwd 用户名 输入密码,和再次确认输入密码注意你输入的密码是不回显的,输错了也不能按取消键取消。两次都输入正确他会提示你密码更新成功,下次登录时就可以用你创建的这个普通用户登录了。2. 普通用户和root用户的区别root用户可以在linux系统下做任何事情,不受限制;普通用户只能做有限的事情。root用户的命令提示符是 #,普通用户的命令提示符是 $2. su指令(switch user)作用:实现该服务器下各个用户之间的切换用法:su 用户su root时,root可以省略,也是切换到root用户普通用户的切换切换需要输入对方的密码;普通用户切换到root需要输入root密码;root用户切换到普通用户不需要密码。二. 文件的访问者对于一个文件而言,访问他的不外乎有三个对象:拥有者(user)、所属组(group)、其他人(other)。这三个对象既可以是root用户,也可以是普通用户三. 文件类型和访问权限我们创建一个file.txt文件和dir目录,并用ls -l得到它们的详细信息1. 文件类型最左边表示的就是文件类型d:文件夹-:普通文件l:软链接(类似Windows的快捷方式)b:块设备文件(例如硬盘、光驱等)p:管道文件c:字符设备文件(例如屏幕等串口设备)s:套接口文件2. 基本权限三个为一组,从左到右分别表示拥有者,所属组和其他人的对这个文件拥有的权限。r:即read。对于文件而言具有读取文件内容的权限;对目录而言具有浏览该目录信息的权限。w:即write。对文件而已具有修改文件内容的权限;对目录而言具有删除和移动目录内容的权限。x:即execute。对文件而言具有执行文件的权限;对目录而言具有进入目录的权限。- 表示不具有该项权限。rwx的相对顺序是不变的x是r和w的先决条件,没有x权限,那么r和w就不能被指向这些权限是针对特定对象的包括:拥有者、所属组、其他人3. root和拥有者、所属组、其他人的关系其实拥有者、所属组和其他人以及他们对应的权限主要针对的是普通用户,root用户的权限是不受他们限制的。即使对这个文件而言root是其他人,并且权限什么都没有即"- - -",root用户依然随便访问这个文件,而普通用户则要严格收到相应的权限的限制。4. 文件权限值的表示方法文件权限还有其他的表示方法,我们上面看得到就是其中的一种:字符表示法。字符表示法字符 说明rwx 可读可写可访问rw- 可读可写r- - 只可读- - - 无权限二进制数值表示方法权限符号(字符表示) 二进制表示rwx 111rw- 110r- - 100- - - 000八进制数值表示法权限符号(字符表示) 八进制表示rwx 7rw- 6r- - 4- - - 05. 文件访问权限的相关设置方法设置文件的访问权限是指设置文件的拥有者、所属组的用户和拥有者、所属组和其他人对该文件的访问权限。只有root和该文件的拥有者才能修改。5.1 chmod指令(change mode)作用:设置文件的访问权限用法:chmod 权限 文件名①:用户表示符+/-/=权限字符这种方法常用于对单个用户的权限进行设置,结合权限的字符表示法。+:向权限范围增加权限代号所表示的权限-:向权限范围取消权限代号所表示的权限=:向权限范围赋予权限代号所表示的权限用户符号:u:拥有者g:拥有者同组用o:其它用户a:所有用户②三位8进制数字适合一次对所有用户的权限进行更改,结合权限的八进制数值表示法,在chmod后面直接加上要设置权限的对应的三位八进制数字即可。5.2 chown指令(change own)作用:修改文件的拥有者用法:chown 用户名 文件名5.3 chgrp指令(change group)作用:修改文件或目录的所属组用法:chgrp 用户组名 文件名6. umask指令(linux mask)这个指令可以查看或修改当前用户的权限掩码,从而来控制我们所创建文件的默认权限。新建文件夹默认权限mask=0666新建目录默认权限mask=0777普通用户的权限掩码(umask)默认为0002,超级用户的默认为0022实际创建的文件的权限:mask & (~umask)7. 粘滞位有这样一种情况,有好几个普通用户共同完成一件工作,他们最后要把他们的工作成果都汇集到root用户里的一个tmp目录里,也就是把他们各自相应的文件拷贝或移到到tmp里。他们对于tmp这个文件而言是其他人,他们要移动或拷贝自己的文件到tmp里就要有写(w)的权限,这个权限不仅能给tmp里添加文件还能删除文件,也就是说其他普通用户可以把你移动到tmp里的文件删除!在w权限下既要保证你们可以把文件都添加到tmp里,又要你们删除别人的文件,两个都想要怎么办呢?为了解决这个问题, Linux引入了粘滞位的概念:当一个目录被设置为"粘滞位"(用chmod + t + 目录名),则该目录下的文件只能由:超级管理员(root)删除该目录的拥有者删除该文件的所属组删除我们把tmp这个目录设置为“粘带位”就可以解决这个问题,这样的话我们放进去的文件只能被tmp目录的所有者(这里是root)、超级用户、文件的拥有者和所属组删除。而不会被其他人输出流,因为其他人,包括我们自己对tmp这个目录的访问权限都是t/T四. 关于权限的总结root用户可以横行霸道,对所有用户的所有文件的访问都无视权限,而普通用户文件的访问要严格遵循他对于这个文件的身份(是拥有者、所属组还是其他人)结合相应的权限来访问。有粘滞位的目录,其他用户虽然有w的权限,但是只能对这个写入文件,不能删除别人存在里面的文件。
一. 函数描述自定义一个函数,传入一个整数n,判断是否为素数。若是返回1,否则返回0。二. 素数定义素数又称质数。一个大于1的自然数,除了1和它自身外,不能被其他自然数整除的数叫做质数;否则称为合数。三. 函数实现一个正整数数要是满足:num = a*b,则a、b其中一个数一定是小于等于num平方根的。所以判断一个数是否是素数,只需看[2, sqrt(num)]内的数能否整除num即可,如果能整除,num就不是素数。sqrt函数的作用是返回一个数的正平方根,该函数包含在头文件math.h中。int IsPrime(int num) { int i = 0; for (i = 2; i <= sqrt(num); i++) { if (0 == num%i) { return 0; } } return 1; }
一. 什么是最大公约数和最小公倍数?最大公约数:指能够整除多个整数的最大正整数。例如8和12的最大公约数为4。最小公倍数:两个或多个整数最小的公共倍数。例如6和24的最小公倍数为24。二者的关系假设x和y的最大公约数是m,最小公倍数是n,则xy=mn。二. 计算两个数的最大公约数和最小公倍数最大公约数:(辗转相除法):令两个数为a和b(不论顺序)。若b不为0,更新a和b的值:把a/b的余数赋值给b,b原来的值赋给a。重复操作,直到b=0,此时a为二者最大公约数。最小公倍数:利用二者的关系(最大公约数 * 最小公倍数=a * b),只需先求出最大公约数即可。测试代码:#include<stdio.h> int gcd(int a, int b)//求最大公约数函数 { int tmp = 0; while (b) { tmp = a % b a = b; b = tmp; } return b; } int main() { int num1 = 4; int num2 = 25; printf("最大公约数为:%d,最小公倍数为%d\n", gcd(num1, num2) , num1*num2/ gcd(num1, num2)); return 0; }运行结果
2023年05月