本笔记用作个人学习和查漏补缺使用,欢迎借鉴学习,期间部分函数介绍,一些知识科普使用网上各位大佬的文章,若未标明引用,请提醒我,如果不能使用,则删除,转载需标注出处www.jjyaoao.space
项目总览
第一板块-如何开发永不停机的服务程序 章节内容 后台开发的重点
程序的异常
永不停机的服务程序
章节任务 生成测试数据
服务程序的调度
守护进程的实现
两个常见小工具
一:生成测试数据 小结任务
全国气象站点参数
全国气象分钟观测数
需求
①:搭建程序框架
运行的参数、说明文档、运行日志
/tmp/idc/surfdata/SURF_ZH_20220514021227_4976.xml
/tmp/idc/surfdata/SURF_ZH_20220416123000_4973.xml
/tmp/idc/surfdata/SURF_ZH_20220514075211_11385.xml
②:加载站点参数
st_stcode结构体,存放站点
创建st_stcode向量实例vstcode
LoadSTCode方法来加入
使用CFile类–自行封装–进行文件读入读出
m_vCmdstr–自行封装–拆分字段
③:模拟观测数据
st_surfdata结构体,实现每个站点的分钟观测
创造st_surfdata向量实例vsurfdata
CrtSurfData函数,实现分钟观测
播随机数种子
获取当前时间,作为观测时间
遍历站点容器–vstcode
随机数填充分钟观测数据的结构体
将结构体放入容器vsurfdata
④:把站点观测数据写入文件
CruSurfFile函数实现写入每分钟的观测数据
生成临时文件名–以
这里outpath是绝对路径,strddatetime是当前时间,getpid是进程号,datafmt是文件格式,进程号主要是为了保证临时文件名不重复(getpid()),这里不加也可以
打开文件
写入第一行标题//csv才需要
遍历存放观测数据的容器vsurfdata,并且,对临时文件进行写入操作
关闭文件
支持csv、xml、json
有漏洞 文件在写入过程中,需要时间,如果其他程序,在这个时候读取了这个文件,就会读取到不完整的内容
正确的 我们用临时副本文件来写入,保证了别的程序目前如果要读取文件,仍然是读取的之前的文件,待临时文件准备好以后,
csv,xml,json
再度超级女生
xml与json比较 可读性 JSON和XML 的可读性可谓不相上下,一边是简易的语法,一边是规范的标签形式,很难分出胜负。
可扩展性 XML天生有很好的扩展性,JSON当然也有,没有什么是XML可以扩展而JSON却不能扩展的。不过JSON在Javascript主场作战,可以存储Javascript复合对象,有着xml不可比拟的优势。
编码难度 XML有丰富的编码工具 ,比如Dom4j、Dom、SAX等,JSON也有提供的工具。无工具的情况下,相信熟练的开发人员一样能很快的写出想要的xml文档和JSON字符 串,不过,xml文档要多很多结构上的字符。
实例比较 XML和JSON都使用结构化方法 来标记数据,下面来做一个简单的比较。
用XML表示中国部分省市数据如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <?xml version="1.0" encoding="utf-8"?> <country > <name > 中国</name > <province > <name > 黑龙江</name > <cities > <city > 哈尔滨</city > <city > 大庆</city > </cities > </province > <province > <name > 广东</name > <cities > <city > 广州</city > <city > 深圳</city > <city > 珠海</city > </cities > </province > <province > <name > 台湾</name > <cities > <city > 台北</city > <city > 高雄</city > </cities > </province > <province > <name > 新疆</name > <cities > <city > 乌鲁木齐</city > </cities > </province > </country >
用JSON表示如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 { "name" : "中国" , "province" : [{ "name" : "黑龙江" , "cities" : { "city" : ["哈尔滨" , "大庆" ] } }, { "name" : "广东" , "cities" : { "city" : ["广州" , "深圳" , "珠海" ] } }, { "name" : "台湾" , "cities" : { "city" : ["台北" , "高雄" ] } }, { "name" : "新疆" , "cities" : { "city" : ["乌鲁木齐" ] } }] }
可以看到,JSON 简单的语法格式和清晰的层次结构明显要比 XML 容易阅读,并且在数据交换方面,由于 JSON 所使用的字符要比 XML 少得多,可以大大得节约传输数据所占用的带宽。
二、服务程序的调度
信号 实例引入信号量 像这样的代码
ctrl + c 和 killall 和 kill + 进程号都可以终止
用ctrl + c 或者 killall命令终止程序的本质,是向正在运行的book程序发出一个信号
如果在book程序中没有处理信号,就会按缺省来处理
linux的信号有64种,大部分的信号缺省处理方法是终止程序运行
可是这令人无法接受=-=,因此我们可以在程序里增加捕获信号的代码,不执行系统缺省的动作,而是调用一个函数
第一次优化 我们引入signal函数 传参的第一个ii,为信号量的具体数值,func代表接收到这个信号后应该采用func函数的方式处理
同时……………….
9的信号是不能被忽略,也不能被屏蔽(不能被signal捕获),是一定会执行的强制杀死程序的信号
将15忽略 / 将15缺省,也就是先后
1、信号基本概念 信号(signal)是软件中断,是进程之间相互传递消息的一种方法,用于通知进程发生了事件,但是,不能给进程传递任何数据。
信号产生的原因有很多,在Linux下,可以用kill和killall命令发送信号。
SIG_DFL,SIG_IGN 分别表示无返回值的函数指针,指针值分别是0和1,这两个指针值逻辑上讲是实际程序中不可能出现的函数地址值。SIG_DFL :默认信号处理 程序SIG_IGN :忽略 信号的处理程序
2、信号的类型
信号名
信号值
默认处理动作
发出信号的原因
SIGHUP
1
A
终端挂起或者控制进程终止
SIGINT
2
A
键盘中断Ctrl+c
SIGQUIT
3
C
键盘的退出键被按下
SIGILL
4
C
非法指令
SIGABRT
6
C
由abort(3)发出的退出指令
SIGFPE
8
C
浮点异常
SIGKILL
9
AEF
采用kill -9 进程编号 强制杀死程序。
SIGSEGV
11
C
无效的内存引用
SIGPIPE
13
A
管道破裂,写一个没有读端口的管道。
SIGALRM
14
A
由alarm(2)发出的信号
SIGTERM
15
A
采用“kill 进程编号”或“killall 程序名”通知程序。
SIGUSR1
10
A
用户自定义信号1
SIGUSR2
12
A
用户自定义信号2
SIGCHLD
17
B
子进程结束信号
SIGCONT
18
进程继续(曾被停止的进程)
SIGSTOP
19
DEF
终止进程
SIGTSTP
20
D
控制终端(tty)上按下停止键
SIGTTIN
21
D
后台进程企图从控制终端读
SIGTTOU
22
D
后台进程企图从控制终端写
处理动作一项中的字母含义如下
A 缺省的动作是终止进程。
B 缺省的动作是忽略此信号,将该信号丢弃,不做处理。
C 缺省的动作是终止进程并进行内核映像转储(core dump),内核映像转储是指将进程数据在内存的映像和进程在内核结构中的部分内容以一定格式转储到文件系统,并且进程退出执行,这样做的好处是为程序员 提供了方便,使得他们可以得到进程当时执行时的数据值,允许他们确定转储的原因,并且可以调试他们的程序。
D 缺省的动作是停止进程,进入停止状态的程序还能重新继续,一般是在调试的过程中。
E 信号不能被捕获。
F 信号不能被忽略。
3、信号的处理 进程对信号的处理方法有三种:
1)对该信号的处理采用系统的默认操作,大部分的信号的默认操作是终止进程。
2)设置中断的处理函数,收到信号后,由该函数来处理。
3)忽略某个信号,对该信号不做任何处理,就像未发生过一样。
signal函数可以设置程序对信号的处理方式。
函数声明:
1 sighandler_t signal (int signum, sighandler_t handler) ;
参数signum表示信号的编号。
参数handler表示信号的处理方式,有三种情况:
1)SIG_DFL:恢复参数signum所指信号的处理方法为默认值。
2)一个自定义的处理信号的函数,信号的编号为这个自定义函数的参数。
3)SIG_IGN:忽略参数signum所指的信号。
4、信号有什么用 服务程序运行在后台,如果想让中止它,杀掉不是个好办法,因为程序被杀的时候,程序突然死亡,没有安排善后工作。
如果向服务程序发送一个信号,服务程序收到这个信号后,调用一个函数,在函数中编写善后的代码,程序就可以有计划的退出。
向服务程序发送0的信号,可以检测程序是否存活。
5、信号应用示例 在实际开发中,在main函数开始的位置,程序员会先屏蔽掉全部的信号。
1 for (int ii=1 ;ii<=64 ;ii++) signal (ii,SIG_IGN);
这么做的目的是不希望程序被干扰。然后,再设置程序员关心的信号的处理函数。
程序在运行的进程中,如果按Ctrl+c,将向程序发出SIGINT信号,编号是2。
采用“kill 进程编号”或“killall 程序名”向程序发出的是SIGTERM信号,编号是15。
采用“kill -9 进程编号”向程序发出的是SIGKILL信号,编号是9,此信号不能被忽略,也无法捕获,程序将突然死亡。
设置SIGINT和SIGTERM两个信号的处理函数,这两个信号可以使用同一个处理函数,函数的代码是释放资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 \#include <stdio.h> \#include <stdlib.h> \#include <string.h> \#include <unistd.h> \#include <signal.h> void EXIT (int sig) { printf ("收到了信号%d,程序退出。\n" ,sig); exit (0 ); } int main () { for (int ii=1 ;ii<=64 ;ii++) signal (ii,SIG_IGN); signal (SIGINT,EXIT); signal (SIGTERM,EXIT); while (true ) { printf ("执行了一次任务。\n" ); sleep (1 ); } }
运行效果
不管是用Ctrl+c还是kill,程序都能体面的退出。
6、发送信号 Linux操作系统提供了kill和killall命令向程序发送信号,C语言也提供了kill库函数,用于在程序中向其它进程或者线程发送信号。
函数声明:
int kill(pid_t pid, int sig);
kill函数将参数sig指定的信号给参数pid 指定的进程。
参数pid 有几种情况:
1)pid>0 将信号传给进程号为pid 的进程。
2)pid=0 将信号传给和目前进程相同进程组的所有进程,常用于父进程给子进程发送信号,注意,发送信号者进程也会收到自己发出的信号。
3)pid=-1 将信号广播传送给系统内所有的进程,例如系统关机时,会向所有的登录窗口广播关机信息。
sig:准备发送的信号代码,假如其值为零则没有任何信号送出,但是系统会执行错误检查,通常会利用sig值为零来检验某个进程是否仍在运行。
返回值说明: 成功执行时,返回0;失败返回-1,errno被设为以下的某个值。
EINVAL:指定的信号码无效(参数 sig 不合法)。
EPERM:权限不够无法传送信号给指定进程。
ESRCH:参数 pid 所指定的进程或进程组不存在。
Linux多进程 0-1-2号进程 0号进程加载完系统后演变成1号和2号进程
1号负责启动系统服务,例如网络服务,防火墙,SSH,ftp服务,早期的系统中也叫init进程
2号线程负责所有内核的调度和管理
进程标识 查看进程 ps -ef可以查看全部的进程信息 加 | 表示管道 再+more的话表示分页,用空格来跳转下一页
UID:启动进程的用户 PID:进程编号 PPID:父进程编号 C:CPU占用率 STIME:进程的开始时间
TTY:启动进程的终端设备(现在不关心了 TIME:进程运行的总时间 CMD:启动进程时执行的命令
1号2号进程他们的父进程是0号,其他的进程的父进程不是一号就是二号
证明:我们用上节课的book来看
获取进程
程序中创建进程 fork是分叉的意思
验证1、2句
验证3、4句
接受了返回值,子进程的返回值是0,父进程的返回值是子进程ID,调用失败返回 -1
返回-1一般是因为:进程太多、内存不足、系统没有资源
我们可以通过返回值不同的这一特性,来使得后续子进程和父进程单独执行他们自己的代码
验证5、6句 我们可以看到,在子进程中ii不断增大,父进程中明明是同一个进程的变量,却不发生改变
第七句
这里引出了一个疑问 ,为什么,我要成为优秀的程序员。这句话执行了两次呢?按照程序的连续性,应该从fork之后,才会分出两个子进程
分析
这一行代码,并没有在打开文件时就写入缓冲区,缓冲区说白了就是内存 ,也就是说fork之前,这一行内容还在内存里,并没有写到文件中去
接下来,父进程的数据空间被复制了一份给子进程,数据空间包括了文件缓冲区,所以fork之后,在父进程的数据空间里面有这行代码的内容,而子进程的文件缓冲区也有这个内容,目前,他们还是把内容放在各自的缓冲区里面。当程序fclose关闭时,再把各自缓冲区的内容写入文件
现在我们来改一下程序
增加了一句flush的意思为刷新—这里刷新缓冲区,这样处理的话,最终生成的文件只会有一行这个输出
现在就是我们预计的结果了
这里我们需要强调一点,父进程和子进程的执行 顺序是不确定的,取决于操作系统的调度算法!
一般来说我们不关心那个跑得更快
另外一点强调,虽然进程之间互称子父进程,但其实互不影响,两个进程之间是独立存在的,没有联系
验证结果
如果互相影响,父进程就算跑得快也最多执行一行,因为sleep1s
另外,最下面那个fclose应该放在pid>0的判断语句里,因为,比如子进程已经fclose了,那么再fclose一次的话可能会导致内存错误(找不到那个进程),我这里太懒了,不再截图了
还有就是,我们也看到了pid == 0哪里最下面的aaa 我要xxx未能执行,因为已经关闭了文件
fprintf 只是对这个函数补充,以前用的少噢
1 2 3 4 5 6 7 8 9 10 11 12 13 14 \#include <stdio.h> \#include <stdlib.h> int main () { FILE * fp; fp = fopen ("file.txt" , "w+" ); fprintf (fp, "%s %s %s %d" , "We" , "are" , "in" , 2014 ); fclose (fp); return (0 ); }
让我们编译并运行上面的程序,这将创建文件 file.txt ,它的内容如下:
现在让我们使用下面的程序查看上面文件的内容:
实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \#include <stdio.h> int main () { FILE *fp; int c; fp = fopen ("file.txt" ,"r" ); while (1 ) { c = fgetc (fp); if ( feof (fp) ) { **break ** ; } printf ("%c" , c); } fclose (fp); return (0 ); }
僵尸进程 我们让子进程先退出
我们看,也就是说2816是父进程,2817是子进程,当子进程先退出,它的标识就变了,但是并没有直接结束进程,要等到父进程也退出了,它才一起走
僵尸进程的危害 简而言之,就是子进程死了,它的进程号还被占用,被存入一个数据结构里面,如果父进程不及时处理,进程号就一直被占用,但系统进程号有限,可能之后会因为没有进程号而不能产生新的进程,这就是僵尸进程的危害
解决方法
忽略SIGCHLD信号,因为子进程退出,内核向父进程发送这个信号,等待处理,如果没收到,自然就没人管咯,父进程不认儿子咯
在父进程中增加等待子进程的代码 wait需要包含的头文件 wait产生的问题:wait会阻塞父进程,迫使父进程必须接收到子进程结束的信号才能进行下一步的操作,这段时间,父进程就干不了其他事情了
在子进程执行结束后,内核给父进程发出这个信号,然后,此时父进程收到了信号(且父进程还在执行sleep10s)就进入func函数,并且sleep的过程被信号软中断强行打断,在函数内定义了int变量sts,后在用sts的地址保存子进程如何退出(地址的每一位,用来保存对应数据啥啥啥的?),由于得到了sig信号,所以能顺利执行wait,并且退出函数,这个时候,由于父进程在sleep10s之后也没有其他的语句了,因此也退出,如果我们要看到效果,可以再sleep10s之后再加入一句sleep10s,父子两就不会同时退出了
wait() 对 wait() 的调用会阻止调用进程,直到它的一个子进程退出或收到信号为止。子进程终止后,父进程在wait系统调用指令后继续执行。 子进程可能由于以下原因而终止:
调用exit();
接收到main进程的return值;
接收一个信号(来自操作系统或另一个进程),该信号的默认操作是终止。
1 2 3 4 语法: pid_t wait (int *stat_loc) ;123
如果任何进程有多个子进程,则在调用 wait() 之后,如果没有子进程终止,则父进程必须处于wait状态。 如果只有一个子进程被终止,那么 wait() 返回被终止的子进程的进程ID。 如果多个子进程被终止,那么 wait() 将获取任意子进程并返回该子进程的进程ID。 wait的目的之一是通知父进程子进程结束运行了,它的第二个目的是告诉父进程子进程是如何结束的。wait返回结束的子进程的PID给父进程。父进程如何知道 子进程是以何种方式退出 的呢? 答案在传给wait的参数之中。父进程调用wait时传一个整型变量地址 给函数。内核将子进程的退出状态保存在这个变量 中。如果子进程调用exit退出,那么内核把exit的返回值存放到这个整数变量中;如果进程是被杀死的,那么内核将信号序号存放在这个变量中。这个整数由3部分 组成,8个bit 记录子进程exit值,7个bit 记录信号序号,另一个bit 用来指明发生错误并产生了内核映像(core dump)。 如果进程没有子进程 ,那么 wait() 返回“**-1**”。
孤儿进程
其实孤儿并不孤儿
当父进程走后,子进程会自动挂到1号进程旗下
服务程序的调度 服务程序一般需要把信号关掉(后台程序,只是执行单一的命令的)
execl() 相关函数:fork, execle, execlp, execv, execve, execvp
头文件:#include <unistd.h>
定义函数:int execl(const char * path, const char * arg, …);
函数说明:execl()用来执行 参数path 字符串所代表的文件路径 , 接下来的参数代表执行该文件时 传递过去的argv(0), argv[1], …, 最后一个参数必须用空指针(NULL)作结束.
返回值:如果执行成功则函数不会返回, 执行失败则直接返回-1, 失败原因存于errno 中.
范例
1 2 3 4 5 #include <unistd.h> main (){ execl ("/bin/ls" , "ls" , "-al" , "/etc/passwd" , (char *)0 ); }
执行:
1 2 -rw-r--r-- 1 root root 705 Sep 3 13 :52 /etc/passwd
execl函数,在执行的过程中,等于是把这个程序终止了,用第一个参数的程序来替代现在正在运行的程序
也就是如下,执行完了aaa以后就停止了,之后从ls里面执行tmp目录下project.tgz
当然,如果我们故意把目录写错,就会继续执行后面的语句,并且execl函数执行错误返回值为**-1**
execl()+wait() exec开头的函数,功能大同小异
每经过fork() == 0时,就执行一次,分支
这里就使得他每间隔10s执行一次ls,对project.tgz 执行-it指令
这样是非常好的,不过也出现了一个问题,我们到底需要兼容多少个参数?难不成一直写判断语句吗
因此,我们引入了execv()
execv() 如果说execl必须明确传参列表,那么execv就可以不用明确,但它底层采用的方式却不是分割字符串这个简单,不过最好空格隔开
同样,也是用第一个参数,开始来代替这个程序接着走下去
main函数传参 argc其实无需传入,它自动显示,是你传的加上该程序本身的个数之和,传入的所有都从argv[1]开始存放,argv[0]为当前程序名字
解释一 这里主要说明带参数的main函数如何使用
1 2 3 4 5 6 7 8 int main (int argc, char * argv[]) { int i; for (i=0 ; i<argc; i++) printf ("%d: %s\r\n" , i+1 , argv[i]); return 0 ; } 1234567
参数介绍 argc : main函数参数个数,当参数为void的时,argc=1,默认参数为可执行文件名 argv : 指针数组,分别指向个参数字符串首地址,其中argv[0]指向默认参数
代码输出结果 没有参数默认输出./hello 有参数按照参数顺序输出参数
解释二 main函数的两个传入参数即
1 2 int main(int argc, char *argv[])
其中argc为一个整数,表示程序传入参数的个数,其英文全称也即是argument_count,简称为了argc。
而argv为一个存放参数值的字符串 数组,其英文全称argument_value,它会根据传入的数据,将传入的数据存入该指针数组。如果有空格隔开,则表示传入多个参数。
比如我们允许main时传入了参数
1 int main("TrainData_2015.1.1_2015.2.19.txt" "input_5flavors_cpu_7days.txt" )
那么传入进到程序中的argc参数则为3了,因为程序会把自己的名字作为第一个参数,示例如下
1 2 3 4 5 6 7 8 9 10 11 12 13 argc = 3 ; argv[0 ] = "filename" ; argv[1 ] = "TrainData_2015.1.1_2015.2.19.txt" ; argv[2 ] = "input_5flavors_cpu_7days.txt" ;
传入文件名后,我们便可以对文件进行读写操作了。
调度服务代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main (int argc,char *argv[]) { if (argc<3 ) { printf ("Using:./procctl timetvl program argv ...\n" ); printf ("Example:/project/tools1/bin/procctl 5 /usr/bin/tar zcvf /tmp/tmp.tgz /usr/include\n\n" ); printf ("本程序是服务程序的调度程序,周期性启动服务程序或shell脚本。\n" ); printf ("timetvl 运行周期,单位:秒。被调度的程序运行结束后,在timetvl秒后会被procctl重新启动。\n" ); printf ("program 被调度的程序名,必须使用全路径。\n" ); printf ("argvs 被调度的程序的参数。\n" ); printf ("注意,本程序不会被kill杀死,但可以用kill -9强行杀死。\n\n\n" ); return -1 ; } for (int i = 0 ; i < 64 ; i++){ signal (i, SIG_IGN); close (i); } if (fork() != 0 ) exit (0 ); signal (SIGCHLD, SIG_DFL); char *pargv[argc]; for (int i = 2 ; i < argc; i++) pargv[i-2 ] = argv[i]; pargv[argc-2 ] = NULL ; while (true ){ if (fork() == 0 ){ execv (argv[2 ], pargv); exit (0 ); }else { int status; wait (&status); sleep (atoi (argv[1 ])); } } }
三、守护进程的实现
Linux共享内存 1.查看共享内存,使用命令:ipcs -m
2.删除共享内存,使用命令:ipcrm -m [shmid]
Linux中,每个内存的内存空间是独立的,互相不能访问,共享内存允许多个内存访问同一块内存,是进程之间共享和传递数据最高效 的方式
共享内存的操作 删除的情况是,除非整个项目的服务程序都要停止运行
每个函数失败都是返回**-1**啦
shmget 第一个参数,key,和共享内存的key是一样的意思,第二个参数是信号量的个数,一般取值为1,第三个参数是创建信号量的权限和他的一些标志
0640是八进制表示,0不能少(看项目情况,这个是权限 ,你需要什么,就写什么),后面那部分(IPC_CREAT)表示共享内存存在 ,就获得他的ID,如果不存在,就创建他(这个基本不能改)
ipcs -m查看内存段 nattch指的是被多少个进程链接了
我们也可以解释为什么key要填16进制,因为如果你填十进制,但是他显示默认是16进制,这样不好区分段,无疑增加了工作量
ipcrm -m xxxshmid 删除内存段
shmat 第一个参数:共享内存的ID,第二个第三个都可以填0 返回值:共享内存的地址,程序中用指针来指 创建一个共享内存结构体,然后来了把当前进程写入共享内存
这个过程就证明了,这个内存空间一直存在,因为最后一步已经剥离了进程(创建好以后(0x5005号),所以后面进来执行都是往5005这个内存块写入东西
shmctl 一般来说,这个指令不止删除一个功能,但我们通常使用中,只会使用到他的删除功能
至少在这个程序里我们不能用,因为我们刚刚创建好,写入,又把它删除没啥意思
Linux信号量 ipcs -s 查看 信号量
ipcrm sem xxxxsemid 删除 信号量
泪目,操作系统的pv操作,居然学到了
引例 信号量形式 CSEM类
PV 共享内存为什么需要PV?,因为在共享内存正在写入的过程中,是不应该允许别的进程访问他的,这样会导致残缺的数据访问,我们说,这不是我们想要看到的
加锁状态
10s以后
查/删信号量
多个进程同时抢共享内存 我们先顺序运行 aaa bbb ccc ddd
最开始496s时 接着我们会发现,ddd的时候,他已经执行了v操作,此时val不是之前演示的1,而是0,是由于bbb此时已经被唤醒,并且得到了这个信号量 可以看到,ddd后面已无等待进程 ,所以执行v操作以后,信号量+1,恢复为默认的1
信号量初始化 引入 初始值为0的话,会使得P操作永远处于等待状态,不过我们可能会想到,先初始化1一个为0的信号量,再把这个信号量的第二个参数设置为1不就好了吗?
但如果此时正在有人执行p操作,你这样赋值,不就把锁给解开了吗
创造具体过程 IPC_EXCL标志写入后,如果信号量已存在,semget这个函数调用后,会调用失败,多进程的程序,一定要考虑他们之间的竞争关系
我们假设有两个进程同时获取,那么他们就会同时往后面走(因为此时信号量不存在,两个都能进入第二个if,如果没有这个IPC_EXCl标记,就会导致,他们都能创建信号量,并且都能设置信号量初始值为1,这看上去,就像各自都持有一把锁 ,所以他们的p操作都能够成功
两个小细节
semget的第二个参数是信号量个数,看需要取 semctl的第二个参数是信号量编号,编号是从0开始,也就是说,如果只有一个信号了,他应该填0
信号量的初始值(value):二值信号量填1,其他信号量看你实际开发中的需求来填
PV操作再深入 我们可以看到,P和V的含义是不同的,但是他们的函数代码完全相同
但是,信号量的值不能够直接加减运算,要用OP函数,OP函数第一个参数是信号量的ID,如果操作的是单个信号量,第二个参数填一个结构体的地址,第三个参数填1,如果操作的是一组信号量,第二个参数填结构体数组的地址,第三个参数填信号量个数
我们再来看结构体,第一个参数num是 信号量编号,第二个是op信号量,第三个见下面sem_flg 在实际使用的过程中,P缺省把信号量的值减一 ,V 缺省把信号量的值加一 (也就是sem_op = -1 / 1),当然也可以不用缺省值
sem_flg看情况用
sem_flg用法解析 如果sem_flg设置为SEM_UNDO,例如对同一个信号量,不同进程操控,在进程未全部终止的时候,进程之间使用该信号量,仍然是上一个进程使用之后的结果,换句话说,也就是不会初始化为init的第三个参数=-=。当所有进程结束的时候,操作系统帮我们执行初始化,这种情况,就适用于IO设备,也就是说,适用于资源有限、不变 的模型
另外,再补充一个UNDO 的细节,在某些情况下可以避免死锁 ,例如,aaa进程,还没用完锁就被杀死了,这个时候有bbb在等着,如果没有UNDO恢复信号量为初始值,那么bbb就死等到生命的结尾,我们说,这也不是我们想要看到的
如果sem_fig为0的话就是另外一个情况,也就是 从0开始嘛,不会初始化,该信号量是多少就是多少,不会去管他,随便你们几个进程兄弟伙咋过整,管我求事,这种感觉。适用于生产者-消费者 模型,动态供给关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include "_public.h" CSEM sem; struct st_pid { int pid; char name[51 ]; }; int main (int argc, char * argv[]) { if (argc < 2 ){ printf ("Using:./book1 procname\n" ); return 0 ; } int shmid; if ((shmid = shmget (0x5005 , sizeof (struct st_pid), 0640 |IPC_CREAT)) == -1 ){ printf ("shmget(0x5005) failed\n" ); return -1 ; } if (sem.init (0x5005 ) == false ){ printf ("sem.init(0x5005) failed\n" ); return -1 ; } struct st_pid * stpid = 0 ; if ((stpid = (struct st_pid *)shmat (shmid,0 ,0 )) == (void *)-1 ){ printf ("shmat failed\n" ); return -1 ; } printf ("aaa time = %d, val = %d\n" , time (0 ), sem.value ()); sem.P (); printf ("bbb time = %d, val = %d\n" , time (0 ), sem.value ()); printf ("pid=%d,name=%s\n" , stpid -> pid, stpid -> name); stpid -> pid = getpid (); sleep (10 ); strcpy (stpid -> name, argv[1 ]); printf ("pid=%d,name=%s\n" , stpid -> pid, stpid -> name); printf ("ccc time = %d, val = %d\n" , time (0 ), sem.value ()); sem.V (); printf ("ddd time = %d, val = %d\n" , time (0 ), sem.value ()); shmdt (stpid); }
守护进程实现 1 g++ -g -o book1 book1.cpp -I/project/public /project/public /_public.cpp
心跳机制 创建一块共享内存,用于存放服务程序心跳信息的结构体数组,每个服务程序启动的时候,会查找共享内存,在共享内存中空白位置把自己的心跳信息写进去,并且程序在运行的过程中,还会不断的把自己的心跳信息写进去,更新到心跳数组中,表示自己是活着,守护进程每隔若干秒,遍历一次共享内存,检查每个服务程序的心跳信息,如果当前时间 - 最后一次心跳时间 > 超时时间 表示该服务程序没有心跳了,死掉了,终止他,死掉的服务程序被终止后,调度程序将重新启动它
实现目标 STRCPY() 安全的copy封装
心跳实现
创建/获取共享内存,大小为n * sizeof(struct st_pinfo)
将共享内存连接到当前进程的地址空间
细节1:这样指到共享内存,我们就可以把它当作结构体数组来用,也可以用地址的运算
共享内存创建后,系统对其初始化,不会有垃圾值,我们可以用for遍历,找到没有用的共享内存,如果有pid为0的,表示为空位置
创建当前进程心跳信息结构体变量,把本进程的信息填进去
更新共享内存中当前进程的心跳时间
把当前进程从共享内存中移去
把共享内存从当前进程中分离
更多细节请欣赏下面打了一天的代码=-=
加入了一些异常处理,封装成了类,使之可以被调用,如果需要报告自己的心跳信息,会造对象,会调add方法和uptatime方法足以
代码里还处理了锁(竞争)的问题,没有空位的问题,id重复的问题……
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 #include "_public.h" #define MAXNUMP_ 1000 #define SHMKEYP_ 0x5095 #define SEMKEYP_ 0x5095 struct st_pinfo { int pid; char pname[51 ]; int timeout; time_t atime; }; class PActive {private : CSEM m_sem; int m_shmid; int m_pos; struct st_pinfo *m_shm ; public : PActive (){ m_shmid = -1 ; m_pos = -1 ; m_shm = 0 ; } bool AddPInfo (const int timeout, const char * pname) ; bool UptATime () ; ~PActive (); }; int main (int argc, char * argv[]) { if (argc < 2 ){ printf ("Using:./book procname\n" ); return 0 ; } int m_shmid = 0 ; if ((m_shmid = shmget (SHMKEYP_*sizeof (struct st_pinfo), MAXNUMP_, 0640 |IPC_CREAT)) == -1 ){ printf ("shmget(%x) failed\n" , MAXNUMP_); return -1 ; } CSEM m_sem; if (m_sem.init (SEMKEYP_) == false ){ printf ("m_sem.init(%x) failed\n" , SEMKEYP_); return -1 ; } struct st_pinfo * m_shm ; m_shm = (struct st_pinfo*)shmat (m_shmid, 0 , 0 ); struct st_pinfo stpinfo ; memset (&stpinfo, 0 , sizeof (struct st_pinfo)); stpinfo.pid = getpid (); STRNCPY (stpinfo.pname, sizeof (stpinfo.pname), argv[1 ], 50 ); stpinfo.timeout = 30 ; stpinfo.atime = time (0 ); int m_pos = -1 ; for (int i = 0 ; i < SHMKEYP_; i++) if (m_shm[i].pid == stpinfo.pid){ m_pos = i; break ; } m_sem.P (); if (m_pos == -1 ) for (int i = 0 ; i < SHMKEYP_; i++){ if (m_shm[i].pid == 0 ){ m_pos = i; break ; } } if (m_pos == -1 ){ m_sem.V (); printf ("共享内存空间已用完\n" ); return -1 ; } memcpy (m_shm+m_pos, &stpinfo, sizeof (struct st_pinfo)); m_sem.V (); while (true ){ m_shm[m_pos].atime = time (0 ); sleep (10 ); } memset (m_shm + m_pos, 0 , sizeof (struct st_pinfo)); shmdt (m_shm); return 0 ; } bool PActive::AddPInfo (const int timeout, const char * pname) { if (m_pos != -1 ) return true ; if ((m_shmid = shmget (SHMKEYP_*sizeof (struct st_pinfo), MAXNUMP_, 0640 |IPC_CREAT)) == -1 ){ printf ("shmget(%x) failed\n" , MAXNUMP_); return false ; } if (m_sem.init (SEMKEYP_) == false ){ printf ("m_sem.init(%x) failed\n" , SEMKEYP_); return false ; } m_shm = (struct st_pinfo*)shmat (m_shmid, 0 , 0 ); struct st_pinfo stpinfo ; memset (&stpinfo, 0 , sizeof (struct st_pinfo)); stpinfo.pid = getpid (); STRNCPY (stpinfo.pname, sizeof (stpinfo.pname), pname, 50 ); stpinfo.timeout = timeout; stpinfo.atime = time (0 ); for (int i = 0 ; i < SHMKEYP_; i++) if (m_shm[i].pid == stpinfo.pid){ m_pos = i; break ; } m_sem.P (); if (m_pos == -1 ) for (int i = 0 ; i < SHMKEYP_; i++){ if (m_shm[i].pid == 0 ){ m_pos = i; break ; } } if (m_pos == -1 ){ m_sem.V (); printf ("共享内存空间已用完\n" ); return false ; } memcpy (m_shm+m_pos, &stpinfo, sizeof (struct st_pinfo)); m_sem.V (); return true ; } bool PActive::UptATime () { if (m_pos !=-1 ) return false ; m_shm[m_pos].atime = time (0 ); return true ; } PActive::~PActive (){ if (m_pos !=-1 ) memset (m_shm + m_pos, 0 , sizeof (struct st_pinfo)); if (m_shm != 0 ) shmdt (m_shm); }
守护程序实现 总体框架呈现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include "_public.h" CLogFile logfile; int main (int argc, char * argv[]) { for (int i = 0 ; i < MAXNUMP; i++){ } return 0 ; }
具体实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 #include "_public.h" CLogFile logfile; int main (int argc, char * argv[]) { if (argc != 2 ) { printf ("\n" ); printf ("Using:./checkproc logfilename\n" ); printf ("Example:/project/tools1/bin/procctl 10 /project/tools1/bin/checkproc /tmp/log/checkproc.log\n\n" ); printf ("本程序用于检查后台服务程序是否超时,如果已超时,就终止它。\n" ); printf ("注意:\n" ); printf (" 1)本程序由procctl启动,运行周期建议为10秒。\n" ); printf (" 2)为了避免被普通用户误杀,本程序应该用root用户启动。\n" ); printf (" 3)如果要停止本程序,只能用killall -9 终止。\n\n\n" ); return 0 ; } CloseIOAndSignal (true ); if (logfile.Open (argv[1 ],"a+" ) == false ){ printf ("logfile.Open(%S) failed.\n" , argv[1 ]); return -1 ; } int m_shmid = 0 ; if ((m_shmid = shmget ((key_t )SHMKEYP, MAXNUMP*sizeof (struct st_procinfo), 0666 |IPC_CREAT)) == -1 ){ logfile.Write ("创建/获取共享内存(%x)失败。\n" , SHMKEYP); return false ; } struct st_procinfo *shm = (struct st_procinfo *)shmat (m_shmid, 0 , 0 ); for (int i = 0 ; i < MAXNUMP; i++){ if (shm[i].pid == 0 ) continue ; int iret = kill (shm[i].pid, 0 ); if (iret == -1 ){ logfile.Write ("进程pid = %d(%S)已经不存在.\n" , (shm+i) -> pid, (shm + i) -> pname); memset (shm+i, 0 , sizeof (struct st_procinfo)); continue ; } time_t now = time (0 ); if (now - shm[i].atime < shm[i].timeout) continue ; logfile.Write ("进程pid = %d(%S)已经超时.\n" , (shm+i) -> pid, (shm + i) -> pname); kill (shm[i].pid, 15 ); for (int j = 0 ; j < 5 ; j++){ sleep (1 ); iret = kill (shm[i].pid, 0 ); if (iret == -1 ) break ; } if (iret == -1 ){ logfile.Write ("进程pid = %d(%S)已经正常终止.\n" , (shm+i) -> pid, (shm + i) -> pname); }else { kill (shm[i].pid, 9 ); logfile.Write ("进程pid = %d(%s)已经强制终止。\n" , (shm+i)->pid, (shm+i)->pname); } memset (shm+i, 0 , sizeof (struct st_procinfo)); } shmdt (shm); return 0 ; }
kill(pid, 0) 答案就是
kill -0 pid 不发送任何信号,但是系统会进行错误检查。
我们可以用来检查一个进程是否存在,存在则 echo $?
返回 0
, 不存在返回 1
当然了,各个系统有自己的稍微差异,比如苹果电脑的
存在则什么都不返回
不存在则
1 2 kill -0 99222 -bash: kill: (99222) - No such process
exit与析构的关系
CPactive 对象,如果放在main 函数中,按下ctrl+c,bbb显示正常退出,但是共享内存(心跳记录)并没有被删除,而把该对象开成全局 变量,按下ctrl+c,终止ddd,ddd就真的被终止了,这是为什么呢?
四、完善生成测试数据程序
增加生成历史数据文件的功能,为压缩文件和清理文件模块准备历史数据文件。
增加信号处理函数,处理2和15的信号
解决调用exit函数退出时局部对象没有调用析构函数的问题
把心跳信息写入共享内存,(虽然说运行很短,根本不需要使用心跳,但我们现在手里只有他,所以就拿他来玩呗)
生成历史数据文件 首先,我们将crtsurfdata5.cpp里面的,再main函数外面的获取当前时间,移植到main里,并且用
1 2 3 4 5 6 memset (strddatetime,0 ,sizeof (strddatetime)); if (argc == 5 ) LocalTime (strddatetime,"yyyymmddhh24miss" ); else STRCPY (strddatetime, sizeof (strddatetime), argv[5 ]);
来进行保存历史时间的功能,这样,数据的时间属性就会根据这个来变化,但是文件的时间属性还没有处理好,在这里,我们的开发框架有一个UTime
我们在关闭文件后,用这个包处理文件的时间就OK了
CrtSurFile(分钟观测写入文件)方法里
处理信号 先关闭所有的信号(放在main函数开头)
有一个细节
这里关闭io一定不能放在打开文件的后面,在前面随便哪个位置都可以,道理很简单,如果把代码放在打开以后,他会把日志文件的文件描述符也给关掉
实例 用了sleep10秒延迟,在关闭写入文件的上面,得以看到结果
运行过程中按ctrl + c
exit未调用析构 我们来分析一下,回顾我们存文件的过程,是先创造一个临时文件,然后往里面写数据,数据写完,再改名为正式的数据文件,在过程中,如果程序被终止,应该把这些文件都清理掉,清理的工作在析构函数中会执行,但如果析构函数没有调用,就会在磁盘上留下这些临时文件
要解决这个问题很简单,把CFile类的 File变成全局对象
我们可以看出,在最初ctrl + c终止后,会存留tmp临时文件,但是将CFile类开到全局,会自动调用析构函数,所以就把tmp删咯
/tmp (20条消息) linux之tmp文件夹_码莎拉蒂 .的博客-CSDN博客_linux tmp
一个小细节,我注意到了ls /tmp/…. 于是乎,我就去搜索了tmp,好家伙
查了些资料,/tmp文件夹是存放linux临时文件 的地方,在Linux系统中/tmp文件夹里面的文件会被清空 ,至于多长时间被清空,如何清空的,可能就不清楚了。
Linux系统中/tmp文件夹里面的文件会被清空,至于多长时间被清空,如何清空的? 今天我们就来剖析一个这两个问题。
在RHEL\CentOS\Fedora\系统中(本次实验是在RHEL6中进行的)
先来看看tmpwatch这个命令,他的作用就是删除一段时间内不使用的文件(removes files which haven’t been accessed for a period of time)。具体的用法就不多说了,有兴趣的自行研究。我们主要看看和这个命令相关的计划任务文件。 它就是/etc/cron.daily/tmpwatch,我们可以看一下这个文件里面的内容:
1 2 3 4 5 6 7 8 9 10 11 12 #! /bin/sh flags=-umc /usr/sbin/tmpwatch "$flags " -x /tmp/.X11-unix -x /tmp/.XIM-unix \ -x /tmp/.font-unix -x /tmp/.ICE-unix -x /tmp/.Test-unix \ -X '/tmp/hsperfdata_*' 10d /tmp /usr/sbin/tmpwatch "$flags " 30d /var/tmp for d in /var/{cache/man,catman}/{cat?,X11R6/cat?,local /cat?}; do if [ -d "$d " ]; then /usr/sbin/tmpwatch "$flags " -f 30d "$d " fi done
第一行相当于一个标记(参数),第二行就是针对/tmp目录里面排除的目录,第三行,这是对这个/tmp目录的清理,下面的是针对其他目录的清理,就不说了。
我们就来看/usr/sbin/tmpwatch “$flags” 30d /var/tmp这一行,关键的是这个30d,就是30天的意思,这个就决定了30天清理/tmp下不访问的文件。如果说,你想一天一清理的话,就把这个30d改成1d。
但有个问题需要注意,如果你设置更短的时间来清理的话,比如说是30分钟、10秒等等,你可以在这个文件中设置,但你会发现重新电脑 ,他不清理/tmp文件夹里面的内容,这是为什么呢?这就是tmpwatch他所在的位置决定的,他的上层目录是/etc/cron.daily/,而这个目录是第天执行一次计划任务,所以说,你设置了比一天更短的时间,他就不起作用了。这下明白了吧。 所以结论是:在RHEL6中,系统自动清理/tmp文件夹的默认时限是30天 。
在Debian\Ubuntu系统中(Ubuntu10.10为实验环境)
在Ubuntu系统中,在/tmp文件夹里面的内容,每次开机都会被清空,如果不想让他自动清理的话,只需要更改rcS文件中的TMPTIME的值。 我们看如何来修改 sudo vi /etc/default/rcS 把 TMPTIME=0 修改成 TMPTIME=-1或者是无限大 改成这样的话,系统在重新启动的时候就不会清理你的/tmp目录了。 依此类推,如果说要限制多少时间来更改的话,就可以改成相应的数字(本人没有测试,我是这么理解的)。
进程的心跳 声明一个CPActive PAcitve;
打开文件,每开始写入一次,记录一条心跳
由于每次时间太短了,虽然20s,但肯定用不完,所以心跳的时间就不用更新了
最终数据程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 #include "_public.h" CPActive PActive; struct st_stcode { char provname[31 ]; char obtid[11 ]; char obtname[31 ]; double lat; double lon; double height; }; vector<struct st_stcode> vstcode; bool LoadSTCode (const char *inifile) ;struct st_surfdata { char obtid[11 ]; char ddatetime[21 ]; int t; int p; int u; int wd; int wf; int r; int vis; }; vector<struct st_surfdata> vsurfdata; char strddatetime[21 ];void CrtsurfData () ;CFile File; bool CrtSurFile (const char *outpath, const char *datafmt) ;CLogFile logfile; void EXIT (int sig) ; int main (int argc, char *argv[]) { if ((argc != 5 ) && (argc != 6 )){ printf ("Using:./crtsurfdata inifile outpath logfile datafmt [datetime]\n" ); printf ("Example:/project/idc1/bin/crtsurfdata /project/idc1/ini/stcode.ini /tmp/idc/surfdata /log/idc/crtsurfdata.log xml,json,csv\n\n" ); printf ("Example:/project/idc1/bin/crtsurfdata /project/idc1/ini/stcode.ini /tmp/idc/surfdata /log/idc/crtsurfdata.log xml,json,csv 20220416123000\n\n" ); printf ("Example:/project/tools1/bin/procctl 60 /project/idc1/bin/crtsurfdata /project/idc1/ini/stcode.ini /tmp/idc/surfdata /log/idc/crtsurfdata.log xml,json,csv\n\n" ); printf ("inifile 全国气象站点参数文件名。\n" ); printf ("outpath 全国气象战点数据文件存放的目录。\n" ); printf ("logfile 本程序运行的日志文件名。\n" ); printf ("datafmt 生成数据文件的格式,支持xml、json和csv三种格式,中间用逗号分隔。\n" ); printf ("datetime 这是一个可选参数,表示生成指定时间的数据和文件\n\n\n" ); return -1 ; } CloseIOAndSignal (true ); signal (SIGINT, EXIT); signal (SIGTERM, EXIT); if (logfile.Open (argv[3 ], "a+" , false ) == false ) { printf ("logfile.Open(%s) failed.\n" , argv[3 ]); return -1 ; } logfile.Write ("crtsurfdata 开始运行. \n" ); PActive.AddPInfo (20 , "crtsurfdata" ); if (LoadSTCode (argv[1 ]) == false ) return -1 ; memset (strddatetime,0 ,sizeof (strddatetime)); if (argc == 5 ) LocalTime (strddatetime,"yyyymmddhh24miss" ); else STRCPY (strddatetime, sizeof (strddatetime), argv[5 ]); CrtsurfData (); if (strstr (argv[4 ], "xml" )!=0 ) CrtSurFile (argv[2 ], "xml" ); if (strstr (argv[4 ], "json" )!=0 ) CrtSurFile (argv[2 ], "json" ); if (strstr (argv[4 ], "csv" )!=0 ) CrtSurFile (argv[2 ], "csv" ); logfile.Write ("crtsurfdata 运行结束。 \n" ); return 0 ; } bool LoadSTCode (const char *inifile) { if (File.Open (inifile, "r" ) == false ){ logfile.Write ("File.Open(%s) failed.\n" , inifile); return false ; } char strBuffer[301 ]; CCmdStr CmdStr; struct st_stcode stcode ; while (true ){ if (File.Fgets (strBuffer, 300 , true ) == false ) break ; CmdStr.SplitToCmd (strBuffer, "," , true ); if (CmdStr.CmdCount () != 6 ) continue ; CmdStr.GetValue (0 , stcode.provname, 30 ); CmdStr.GetValue (1 , stcode.obtid, 10 ); CmdStr.GetValue (2 , stcode.obtname, 30 ); CmdStr.GetValue (3 , &stcode.lat); CmdStr.GetValue (4 , &stcode.lon); CmdStr.GetValue (5 , &stcode.height); vstcode.push_back (stcode); } return true ; } void CrtsurfData () { srand (time (0 )); struct st_surfdata stsurfdata ; for (int i=0 ;i<vstcode.size ();i++) { memset (&stsurfdata,0 ,sizeof (struct st_surfdata)); strncpy (stsurfdata.obtid,vstcode[i].obtid,10 ); strncpy (stsurfdata.ddatetime,strddatetime,14 ); stsurfdata.t=rand ()%351 ; stsurfdata.p=rand ()%265 +10000 ; stsurfdata.u=rand ()%100 +1 ; stsurfdata.wd=rand ()%360 ; stsurfdata.wf=rand ()%150 ; stsurfdata.r=rand ()%16 ; stsurfdata.vis=rand ()%5001 +100000 ; vsurfdata.push_back (stsurfdata); } } bool CrtSurFile (const char *outpath, const char *datafmt) { CFile File; char strFileName[301 ]; sprintf (strFileName, "%s/SURF_ZH_%s_%d.%s" , outpath, strddatetime, getpid (), datafmt); if (File.OpenForRename (strFileName, "w" ) == false ){ logfile.Write ("File.OpenForRename(%s) failed.\n" , strFileName); return false ; } if (strcmp (datafmt, "csv" ) == 0 ) File.Fprintf ("站点代, 数据时间, 气温, 气压, 相对湿度, 风向, 风速, 降雨量, 能见度\n" ); for (int i = 0 ; i < vsurfdata.size (); i++){ if (strcmp (datafmt, "csv" ) == 0 ){ File.Fprintf ("%s, %s, %.1f, %.1f, %d, %d, %.1f, %.1f, %.1f\n" ,\ vsurfdata[i].obtid, vsurfdata[i].ddatetime, vsurfdata[i].t / 10.0 , vsurfdata[i].p / 10.0 , \ vsurfdata[i].u, vsurfdata[i].wd, vsurfdata[i].wf / 10.0 , vsurfdata[i].r / 10.0 , vsurfdata[i].vis / 10.0 ); } if (strcmp (datafmt, "xml" ) == 0 ) File.Fprintf ("<obtid>%s</obtid><ddatetime>%s</ddatetime><t>%.1f</t><p>%.1f</p>" \ "<u>%d</u><wd>%d</wd><wf>%.1f</wf><r>%.1f</r><vis>%.1f</vis><endl/>\n" ,\ vsurfdata[i].obtid,vsurfdata[i].ddatetime,vsurfdata[i].t/10.0 ,vsurfdata[i].p/10.0 ,\ vsurfdata[i].u,vsurfdata[i].wd,vsurfdata[i].wf/10.0 ,vsurfdata[i].r/10.0 ,vsurfdata[i].vis/10.0 ); if (strcmp (datafmt, "json" ) == 0 ) { File.Fprintf ("{\"obtid\":\"%s\",\"ddatetime\":\"%s\",\"t\":\"%.1f\",\"p\":\"%.1f\"," \ "\"u\":\"%d\",\"wd\":\"%d\",\"wf\":\"%.1f\",\"r\":\"%.1f\",\"vis\":\"%.1f\"}" ,\ vsurfdata[i].obtid,vsurfdata[i].ddatetime,vsurfdata[i].t/10.0 ,vsurfdata[i].p/10.0 ,\ vsurfdata[i].u,vsurfdata[i].wd,vsurfdata[i].wf/10.0 ,vsurfdata[i].r/10.0 ,vsurfdata[i].vis/10.0 ); if (i<vsurfdata.size ()-1 ) File.Fprintf (",\n" ); else File.Fprintf ("\n" ); } if (strcmp (datafmt,"xml" )==0 ) File.Fprintf ("</data>\n" ); if (strcmp (datafmt,"json" )==0 ) File.Fprintf ("]}\n" ); } File.CloseAndRename (); UTime (strFileName, strddatetime); logfile.Write ("生成数据文件%s成功,数据时间%s,记录数%d.\n" , strFileName, strddatetime, vsurfdata.size ()); return true ; } void EXIT (int sig) { logfile.Write ("程序退出,sig = %d\n\n" , sig); exit (0 ); }
五、开发常用小工具
压缩文件模块 开发框架 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include "_public.h" void EXIT (int sig) ; int main (int argc,char *argv[]) { while (true ){ } return 0 ; } void EXIT (int sig) { printf ("程序退出,sig=%d\n\n" , sig); exit (0 ); }
关闭信号和IO 我们通常写代码时需要先打开 ,调试完毕再关,因为我们的printf这些东西,都必须要打开IO才能显示给我们自己
与超时时间点比较 一个细节,除了判断文件的时间,还要判断文件名,如果文件已经被压缩了,就不需要再压缩了
压缩文件 1>/dev/null 2>/dev/null是我们常用的写法,也就是,把标准输出(1)和标准错误(2)都定位到空里面去,也就是说,不要输出任何东西
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 char strCmd[1024 ]; while (true ){ if (Dir.ReadDir () == false ) break ; if ((strcmp (Dir.m_ModifyTime, strTimeOut) < 0 ) && (MatchStr (Dir.m_FileName, "*.gz" ) == false )){ SNPRINTF (strCmd, sizeof (strCmd), 1000 , "/usr/bin/gzip -f %s 1>/dev/null 2>/dev/null" , Dir.m_FullFileName); if (system (strCmd) == 0 ) printf ("gzip %s ok.\n" , Dir.m_FullFileName); else printf ("gzip %s failed.\n" , Dir.m_FullFileName); }
CDir OpenDir() 打开目录的指令
第一个参数,目录名,第二个参数,文件名匹配的规则(调用了matchstr),第三个参数,获取文件的最大数量,为什么需要这个参数呢?
是因为OpenDir将获取的文件名统一存放在一个容器里(m_vFileName),如果容器过大,业务处理的时候,可能并不需要一次将全部文件都读取出来,他可以一批一批的处理,一次处理1w个….这样的话就不会对内存造成很大的压力
第四个参数,是否打开各级子目录,第五个参数,是否对文件排序(能不排就不排吧)
SetDateFMT() 设置时间格式
控制那容器属性那几个的输出格式
MatchStr() system() 很简单,就一个参数—-你需要的命令
细节一:system()也可以调用其它函数,那么他与exec的其它函数有啥区别呢?
本质上其实是没有区别的,是否可以取代呢?
也不是,exec哪些会更加强大,有更多的功能
细节二:是否需要为压缩文件写心跳信息呢?
一般是不用的,原因如下
这种程序一般是不会死机的,容易卡死的才需要调度
并不好写,因为有的文件很大,压缩的时间很长,有的短,没有一个合理的超时时间标准
压缩文件实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 #include "_public.h" void EXIT (int sig) ;int main (int argc,char *argv[]) { if (argc != 4 ) { printf ("\n" ); printf ("Using:/project/tools1/bin/gzipfiles pathname matchstr timeout\n\n" ); printf ("Example:/project/tools1/bin/gzipfiles /log/idc \"*.log.20*\" 0.02\n" ); printf (" /project/tools1/bin/gzipfiles /tmp/idc/surfdata \"*.xml,*.json\" 0.01\n" ); printf (" /project/tools1/bin/procctl 300 /project/tools1/bin/gzipfiles /log/idc \"*.log.20*\" 0.02\n" ); printf (" /project/tools1/bin/procctl 300 /project/tools1/bin/gzipfiles /tmp/idc/surfdata \"*.xml,*.json\" 0.01\n\n" ); printf ("这是一个工具程序,用于压缩历史的数据文件或日志文件。\n" ); printf ("本程序把pathname目录及子目录中timeout天之前的匹配matchstr文件全部压缩,timeout可以是小数。\n" ); printf ("本程序不写日志文件,也不会在控制台输出任何信息。\n" ); printf ("本程序调用/usr/bin/gzip命令压缩文件。\n\n\n" ); return -1 ; } signal (SIGINT, EXIT); signal (SIGTERM, EXIT); char strTimeOut[21 ]; LocalTime (strTimeOut, "yyyy-mm-dd hh24:mi:ss" , 0 -(int )(atof (argv[3 ])*24 *60 *60 )); CDir Dir; if (Dir.OpenDir (argv[1 ], argv[2 ], 10000 , true ) == false ){ printf ("Dir.OpenDir(%s) failed.\n" , argv[1 ]); return -1 ; } char strCmd[1024 ]; while (true ){ if (Dir.ReadDir () == false ) break ; if ((strcmp (Dir.m_ModifyTime, strTimeOut) < 0 ) && (MatchStr (Dir.m_FileName, "*.gz" ) == false )){ SNPRINTF (strCmd, sizeof (strCmd),1000 , "/usr/bin/gzip -f %s 1>/dev/null 2>/dev/null" , Dir.m_FullFileName); if (system (strCmd) == 0 ) printf ("gzip %s ok.\n" , Dir.m_FullFileName); else printf ("gzip %s failed.\n" , Dir.m_FullFileName); } } return 0 ; } void EXIT (int sig) { printf ("程序退出,sig=%d\n\n" , sig); exit (0 ); }
清理历史数据文件 与压缩文件的实现流程,说白了完全相同,只有最后一步不同
压缩文件是调用system执行一个操作系统命令
删除文件是调用c语言的一个函数来删除
remove() REMOVE() 删除不掉,会重复执行一两次删除命令(通常不超过三次,自己定义次数)
RENAME() 优势:
如果不存在该文件名,会重复执行一两次改名命令(通常不超过三次,自己定义次数)
在以前不存在,rename就会失败,现在这个,失败了就用MKDIR创建文件,因此减少了很多麻烦
代码实现 仅与压缩在最后核心步骤,以及注解之类的存在差异
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 #include "_public.h" void EXIT (int sig) ;int main (int argc,char *argv[]) { if (argc != 4 ) { printf ("\n" ); printf ("Using:/project/tools1/bin/deletefiles /gpathname matchstr timeout\n\n" ); printf ("Example:/project/tools1/bin/deletefiles /log/idc \"*.log.20*\" 0.02\n" ); printf (" /project/tools1/bin/deletefiles /tmp/idc/surfdata \"*.xml,*.json\" 0.01\n" ); printf (" /project/tools1/bin/procctl 300 /project/tools1/bin/deletefiles /log/idc \"*.log.20*\" 0.02\n" ); printf (" /project/tools1/bin/procctl 300 /project/tools1/bin/deletefiles /tmp/idc/surfdata \"*.xml,*.json\" 0.01\n\n" ); printf ("这是一个工具程序,用于删除历史的数据文件或日志文件。\n" ); printf ("本程序把pathname目录及子目录中timeout天之前的匹配matchstr文件全部删除,timeout可以是小数。\n" ); printf ("本程序不写日志文件,也不会在控制台输出任何信息。\n\n\n" ); return -1 ; } CloseIOAndSignal (true ); signal (SIGINT, EXIT); signal (SIGTERM, EXIT); char strTimeOut[21 ]; LocalTime (strTimeOut, "yyyy-mm-dd hh24:mi:ss" , 0 -(int )(atof (argv[3 ])*24 *60 *60 )); CDir Dir; if (Dir.OpenDir (argv[1 ], argv[2 ], 10000 , true ) == false ){ printf ("Dir.OpenDir(%s) failed.\n" , argv[1 ]); return -1 ; } char strCmd[1024 ]; while (true ){ if (Dir.ReadDir () == false ) break ; if (strcmp (Dir.m_ModifyTime, strTimeOut) < 0 ){ if (REMOVE (Dir.m_FullFileName) == true ) printf ("REMOVE %s ok.\n" , Dir.m_FullFileName); else printf ("REMOVE %s failed.\n" , Dir.m_FullFileName); } } return 0 ; } void EXIT (int sig) { printf ("程序退出,sig=%d\n\n" , sig); exit (0 ); }
六、服务程序的运行策略 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # # 启动数据中心后台服务程序的脚本。 # # 检查服务程序是否超时,配置在/etc/rc.local中由root用户执行。 # /project/tools1/bin/procctl 30 /project/tools1/bin/checkproc # 压缩数据中心后台服务程序的备份日志。 /project/tools1/bin/procctl 300 /project/tools1/bin/gzipfiles /log/idc "*.log.20*" 0.02 # 生成用于测试的全国气象站点观测的分钟数据。 /project/tools1/bin/procctl 60 /project/idc1/bin/crtsurfdata /project/idc/ini/stcode.ini /tmp/idc/surfdata /log/idc/crtsurfdata.log xml,json,csv # 清理原始的全国气象站点观测的分钟数据目录/tmp/idc/surfdata中的历史数据文件。 /project/tools1/bin/procctl 300 /project/tools1/bin/deletefiles /tmp/idc/surfdata "*" 0.02
1 2 3 4 5 6 7 8 9 10 11 12 # # 停止数据中心后台服务程序的脚本。 # killall -9 procctl killall gzipfiles crtsurfdata deletefiles sleep 3 killall -9 gzipfiles crtsurfdata deletefiles
注意 :这些start,kill脚本,是跟项目,业务相关的,也就是说,我们一定要放在对应的idc1里面,不能乱 放
现在我们知道了如何在命令行启动脚本实现服务程序的运行调度
现在我们来看看如何在操作系统启动的时候把全部的服务程序运行起来
Linux下面启动程序的方法比较多,比如说采用系统服务,crontab,但是要启动项目,还是我们这种脚本的方式合适
检查服务程序是否超时 /project/tools/bin/procctl 30 /project/tools/bin/checkproc
启动数据中心的后台服务程序 设置开机自启脚本 1.在/etc/init.d/目录下编写脚本 名字任意
1 2 cd /etc/init.d touch xxx.sh
复制成功
1 2 3 4 5 6 7 8 9 10 必须要添加 应该是为了设置 defaults的值 否则不成功#! /bin/sh # # Provides: OnceDoc # Required-Start: $network $remote_fs $local_fs # Required-Stop: $network $remote_fs $local_fs # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: start and stop node # Description: OnceDoc #
添加完上面的文本之后在脚本里写下要自启动的命令,完成后保存。 3.给脚本添加执行权限chmod 777 xxx.sh
4.添加进开机自启项update-rc.d xxx.sh defaults number
这里的number是启动顺序,在某些情况下后有先后顺序的要求。
完成后重启。
*移除开机自启:update-rc.d -f xxx remove
调度 程序由root 启动,服务 程序由jjyaoao 启动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 "project01.sh" # ! /bin/sh # # Provides: OnceDoc # Required-Start: $network $remote_fs $local_fs # Required-Stop: $network $remote_fs $local_fs # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: start and stop node # Description: OnceDoc # # 检查服务程序是否超时 /project/tools/bin/procctl 30 /project/tools/bin/checkproc # 启动数据中心的后台服务程序 su - jjyaoao -c "/bin/sh /project/idc1/c/start.sh"
第二板块-基于ftp协议的文件传输系统
章节内容 一、ftp基础知识 二、ftp客户端封装 三、文件下载功能实现 四、文件上传功能实现 ftp协议是否过时? 网上有这么多的缺点例举…..并且说的也没错
可这并不表示ftp就会被淘汰
就好比,铁门坚固,还有密码,但也不可能取代普通木门一样
适用场合不同
ftp不需要考虑这么多的安全性,也不需要这么多的效率,只是用来实现文件的交换而已
技术和应用场景要放在一起讨论,只看缺点不看优点,是不合适的。
ftp查看状态 1 sudo /etc/init.d/vsftpd status
一、FTP基础知识
FTP简介
FTP工作原理
控制连接在整个文件传送过程当中都是保持打开的,ftp客户发出的传送请求,都要通过客户的控制连接来发送给服务器端的控制进程,所以控制连接相当于正式连接之前的一个准备步骤,而数据连接才是文件传输过程中的实际连接
服务器端的控制进程接收到客户端的数据传输请求后,才创建一个数据传送进程,并且创建数据连接
由于控制连接和数据连接是区分开的,因此我们也说ftp的控制信息是带外传送的
主动:服务器端接收到客户端的端口号,然后建立控制连接关系,服务器端主动告诉客户端,它自己的端口号,这样就是20.
被动:如果是建立联系之后,客户端向服务端发送命令,提出自己的需求,那么服务器端一般就会安排一个>1024端口号的端口来进行连接。
传输模式 二、ftp客户端封装 基本连接 在这里我采用的是,用root账户来连接jjyaoao账户,模拟远程访问的过程
https://github.com/codebrainz/ftplib
将上面的c代码进一步封装成cpp的库_ftp.h,_ftp.cpp
1 g++ -g -o ftpclient ftpclient.cpp /project/public/_ftp.cpp /project/public/_public.cpp -I/project/public -L/project/public -lftp -lm -lc
会遇到链接动态库问题=-=
linux环境变量LD_LIBRARY_PATH - 小 楼 一 夜 听 春 雨 - 博客园 (cnblogs.com)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include "_ftp.h" Cftp ftp; int main () { if (ftp.login ("192.168.211.130:21" , "jjyaoao" , "gh" ) == false ){ printf ("ftp.login(192.168.211.130:21) failed.\n" ); return -1 ; } printf ("ftp.login(192.168.211.130:21) ok.\n" ); ftp.logout (); return 0 ; }
测试size(),time()
这里他的尺寸是对的,但是时间,=-=,我还不太清楚底层,感觉奇特
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include "_ftp.h" Cftp ftp; int main () { if (ftp.login ("192.168.211.130:21" , "jjyaoao" , "gh" ) == false ){ printf ("ftp.login(192.168.211.130:21) failed.\n" ); return -1 ; } printf ("ftp.login(191.168.211.130:21) ok.\n" ); if (ftp.mtime ("/project/public/socket/demo01.cpp" ) == false ){ printf ("ftp.mtime(/project/public/socket/demo01.cpp) failed.\n" ); return -1 ; } printf ("ftp.mtime(/project/public/socket/demo01.cpp) ok, mtime = %d.\n" , ftp.m_mtime); if (ftp.size ("/project/public/socket/demo01.cpp" ) == false ){ printf ("ftp.size(/project/public/socket/demo01.cpp) failed.\n" ); return -1 ; } printf ("ftp.size(/project/public/socket/demo01.cpp) ok, size = %d.\n" , ftp.m_size); ftp.logout (); return 0 ; }
测试nlist() 意思就是只列出子目录,和文件名,不会列出子目录中的文件名
将socket中的文件名,列到了bbb.lst清单里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include "_ftp.h" Cftp ftp; int main () { if (ftp.login ("192.168.211.130:21" , "jjyaoao" , "gh" ) == false ){ printf ("ftp.login(192.168.211.130:21) failed.\n" ); return -1 ; } printf ("ftp.login(191.168.211.130:21) ok.\n" ); if (ftp.mtime ("/project/public/socket/demo01.cpp" ) == false ){ printf ("ftp.mtime(/project/public/socket/demo01.cpp) failed.\n" ); return -1 ; } printf ("ftp.mtime(/project/public/socket/demo01.cpp) ok, mtime = %d.\n" , ftp.m_mtime); if (ftp.size ("/project/public/socket/demo01.cpp" ) == false ){ printf ("ftp.size(/project/public/socket/demo01.cpp) failed.\n" ); return -1 ; } printf ("ftp.size(/project/public/socket/demo01.cpp) ok, size = %d.\n" , ftp.m_size); if (ftp.nlist ("/project/public/socket" , "/aaa/bbb.lst" ) == false ){ printf ("ftp.nlist(/project/public/socket) failed.\n" ); return -1 ; } printf ("ftp.nlist(/project/public/socket) ok.\n" ); ftp.logout (); return 0 ; }
上传下载 get()具体细节 get函数也就是ftp的下载
第一个参数,ftp服务器上的文件名,第二个参数,保存到本地想要采用的文件名,第三个参数,默认true,核对发送前后的时间,以便确保完整发送
下载采用临时文件命名的方法,即后缀+ .tmp,完成后才正式改为localfilename(第二个参数)
put()具体细节 第一个参数是本地待发送的文件的文件名,第二个参数是想要发送到ftp服务器上显示的文件名,第三个是核对本地与远程文件的大小是否相同
get/put差异 现在可能我们会有一个疑问,为什么上传的时候,采用的是核对文件的大小,下载却是核对文件的时间
一个文件是否发生了变换,只能用文件的时间来判断,不能用文件的大小,比如说把文件aaa改成了文件bbb,大小是一样的,文件的时间就不一样了
在上传文件的函数中,服务器上文件的时间,是上传这个动作,也就是调用FtpPut这个函数的时间,这个时间是没有意义的,我们可以保证本地的文件在上传中不会发生变换,所以只要比较服务器中最后收到的文件的大小和本地的文件大小相同就可以了
get/put测试
这样,就在本地下载了_public.cpp, 在本地显示的名字是_public.cpp.bak
也将本地的ftpclient.cpp上传到了ftp服务器,ftp服务器中文件名为ftpclient.cpp.bak
三、文件下载功能实现 我们先思考第一步,从简单的功能出发,逐渐拓展到复杂的功能
makefile问题分析 实现之前,我们先编辑makefile文件,出现了一个问题
听着感觉,好像是libftp.a无法调用啥啥的,好像是个动态库?
我们先来看看.a文件是啥,之前听过,就是对开源框架的封装,将其多个.h.cpp封装到.a里?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 linux .o,.a,.so .o,是目标文件,相当于windows中的.obj文件 .so 为共享库,是shared object,用于动态连接的,相当于windows下的dll .a为静态库,是好多个.o合在一起,用于静态连接 静态函数库 特点:实际上是简单的普通目标文件的集合,在程序执行前就加入到目标程序中。 优点:可以用以前某些程序兼容;描述简单;允许程序员把程序link起来而不用重新编译代码,节省了重新编译代码的时间(该优势目前已不明显);开发者可以对源代码保密;理论上使用ELF格式的静态库函数生成的代码可以比使用共享或动态函数库的程序运行速度快(大概1%-5%) 生成:使用ar程序(archiver的缩写)。ar rcs my_lib.a f1.o f2.o是把目标代码f1.o和f2.o加入到my_lib.a这个函数库文件中(如果my_lib.a不存在则创建) 使用:用gcc生成可执行代码时,使用-l参数指定要加入的库函数。也可以用ld命令的-l和-L参数。
其次,我就去求助广大网友,最终通过三篇文章解决问题
(20条消息) CFLAGS详解_xinyuan0214的博客-CSDN博客_cflags编写makefile
[(20条消息) Resolve “`.rodata’ can not be used when making a PIE object; recompile with -fPIC”_如月灵的博客-CSDN博客](https://blog.csdn.net/hanyulongseucas/article/details/87715186 )
(20条消息) “recompile with -fPIC” 错误,及c代码引用C++库_xinyu391的博客-CSDN博客
解决办法就是,在CFLAG宏里面加入 -no-pie
目标一 目前,我们的目标是把服务器上某目录的文件全部下载到本地目录 (可以指定文件名的匹配规则)
那么,我们现在就要思考,我们需要传入什么参数,首先,肯定需要日志文件名 ,其次,ftp的服务器IP和端口,传输模式,FTP的用户名,密码,服务器存放文件的目录,本地存放文件的目录,下载文件名匹配的规则,都是我们需要的,这样数一数,就已经八个参数了,难道我们要一一对应匹配输入吗?我们说这是不现实的,因此,我们采用一种新的方法,使用xml格式,一方面方便拓展,二方面不会输错,考虑顺序之类的
因此,我们现在只需要两个参数,一个是日志文件名(自定义),另外一个就是这个xml格式的文档,将内容放进去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include "_public.h" #include "_ftp.h" CLogFile logfile; Cftp ftp; void EXIT (int sig) ;void _help();int main (int argc, char *argv[]) { if (argc != 3 ){ _help(); return -1 ; } for (int i = 0 ; i < vlistfile.size (); i++){ } ftp.logout (); return 0 ; } void EXIT (int sig) { printf ("程序退出, sig = %d\n\n" , sig); exit (0 ); }
打开日志文件 虽然和别的之前的也一样,不过这里我忍不住提一句,由于水平上升了,才有机会在这个地方咬文嚼字。
a+,w+,r+到底是什么意思,知道是文件的读写方式,不过今天去了解了更多
r:以只读的方式打开文本文件,文件必须存在;
w:以只写的方式打开文本文件,文件若存在则清空 文件内容从 文件头部 开始写,若不存在则根据文件名创建新文件并只写打开;
a:以只写的方式打开文本文件,文件若存在则从文件尾部以追加 的方式开始写,文件原来存在的内容不会清除 (除了 文件尾标志EOF ),若不存在则根据文件名创建新文件并只写打开;
r+:以可读写的方式打开文本文件,文件必须存在;
w+:以可读写的方式打开文本文件,其他与w一样;
a+:以可读写的方式打开文本文件,其他与a一样;
若打开二进制文件,可在后面加个b注明,其他一样,如rb,r+b(或rb+)。
解析xml 使用xml,就需要先解析他,开发框架中,有解析的函数
如果第三个参数是字符串,可以用第四个参数 指定字符串的长度,缺省为0 ,表示不限长度 ,不限长度就一定要确保value数组空间足够大,否则发生内存溢出的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 bool _xmltoarg(char *strxmlbuffer){ memset (&starg,0 ,sizeof (struct st_arg)); GetXMLBuffer (strxmlbuffer,"host" ,starg.host,30 ); if (strlen (starg.host)==0 ) { logfile.Write ("host is null.\n" ); return false ; } GetXMLBuffer (strxmlbuffer,"mode" ,&starg.mode); if (starg.mode!=2 ) starg.mode=1 ; GetXMLBuffer (strxmlbuffer,"username" ,starg.username,30 ); if (strlen (starg.username)==0 ) { logfile.Write ("username is null.\n" ); return false ; } GetXMLBuffer (strxmlbuffer,"password" ,starg.password,30 ); if (strlen (starg.password)==0 ) { logfile.Write ("password is null.\n" ); return false ; } GetXMLBuffer (strxmlbuffer,"remotepath" ,starg.remotepath,300 ); if (strlen (starg.remotepath)==0 ) { logfile.Write ("remotepath is null.\n" ); return false ; } GetXMLBuffer (strxmlbuffer,"localpath" ,starg.localpath,300 ); if (strlen (starg.localpath)==0 ) { logfile.Write ("localpath is null.\n" ); return false ; } GetXMLBuffer (strxmlbuffer,"matchname" ,starg.matchname,100 ); if (strlen (starg.matchname)==0 ) { logfile.Write ("matchname is null.\n" ); return false ; } return true ; }
1 "<host>127.0.0.1:21</host><mode>1</mode><username>jjyaoao</username><password>gh</password><localpath>/idcdata/surfdata</localpath><remotepath>/tmp/idc/surfdata</remotepath><matchname>SURF_ZH*.XML,SURF_ZH*.CSV</matchname><listfilename>/idc/data/ftplist/ftpgetfiles_surfdata.list<listfilename>"
存放目录的细节 有两种写代码的方式:
那我们来想一想,是返回全部路径好,还是相对路径好呢?
答案是:相对路径(只返回文件名)好
有以下几点原因:
如果加上绝对路径,则会增加相当一部分没有必要的带宽,从而加重网络的负担
我们调用这个方法,已经往里面传入了保存文件的路径,也就是/tmp/idc/surfdata为我们已经知道的东西,再把它写入,是没有必要的举措
所以,我们先通过chdir函数,进入starg.remotepath(目录),再nlist得到当前目录的所有文件+目录名,返回是最好的举措。
1 2 /project/tools1/bin/procctl 60 /project/idc1/bin/crtsurfdata /project/idc/ini/stcode.ini /tmp/idc/surfdata /log/idc/crtsurfdata.log xml,json,csv
1 /project/tools1/bin/procctl 60 /project/idc1/bin/crtsurfdata /project/idc1/ini/stcode.ini /tmp/idc/surfdata /log/idc/crtsurfdata.log xml,json,csv
下载文件的一个细节 踩得大坑,还好自己把他调出来了
百思不得其解,这咋失败了
原来是对细节把握的不到位,有了路径,和文件名,中间得加 / 呀=-=我还以为/是啥新的特殊用法,结果=-=,悟了误了
目标扩充 但是在,实际应用过程中,文件下载功能,不会这么简单……
会有更多的需求。
删除/备份文件 删除文件十分简单,只需要调用ftp的一个方法,下载文件并备份就稍微复杂一点点,需要额外多一个备份目录,并使用strremotefilenamebak来暂存新的目录名加文件名,然后再运用ftprename函数,对strremotefilename改名,改成这个xxxxxbak,就ok了
增量下载文件 相对困难的是增量下载文件,我们先来理解一下这个过程。
首先需要四个vector容器,第一 个容器,存放已成功下载 的文件,程序第一次运行的时候,这个容器肯定是空的,第二 个容器,存放nlist返回的结果,也就是当前服务端的文件,第二个容器中有,第一个容器中没有的文件,放在第四个文件(待下载),第二个容器中有,第一个容器中也有的放在第三个容器(不需要下载的,方便区分?)
第一个状态:服务端五个文件,客户端没有
通过上面规则对比,我们得到第二个状态:
第二次运行时的初始状态如下:可能这个时候你就会问了,为什么服务段的1,2不在了呢?这很简单,因为服务端 也要清理历史文件 嘛,不然文件越堆越多
然后程序把6,7下载下来,3和4不需要下载 了
第三次运行:可能我们现在会对第一个容器产生疑问,我们只需要思考一下,连服务端都没有一和二了,那么客户端还有没有必要保留一和二呢?这就好比游戏,服务端开发已经取消了一个功能,那么制作GUI的客户端自然可以把那个功能相应的按键都删除掉,虽然保留按键也没算错,但这样会让容器变大,让客户以为还有那个功能,这实在是没有必要的。
以下是处理细节:PS:ptype == 1 即为访问,然后什么都不做,不删除也不备份
注意,这个意思就是,已下载的3,就是下一次的已下载1,待下载4,就是这一次需要下载的内容,由于我们之前nlist得到的,是存放在vlistfile2里面,所以,为了和后面遍历vlistfile2,读取,下载的过程兼容,我们把待下载容器4里面的内容放入容器2里面,从而使得后面的代码不用修改!
我们把这四步拆解,第一步一个函数LoadOKFile() 第二步CompVector() 第三步WriteToOKFile() 第四步直接
1 vlistfile2.clear (); vlistfile2.swap (vlistfile4);
另外,由于我们第三步,已经更新了OKFile,因此,我们还需要加入一条函数语句,当ptype == 1时,把下载成功的文件记录追加到okfilename文件中去,下面这个是执行的命令
1 /project/tools1/bin/ftpgetfiles /log/idc/ftpgetfiles_surfdata.log "<host>127.0.0.1:21</host><mode>1</mode><username>jjyaoao</username><password>gh</password><localpath>/tmp/client</localpath><remotepath>/tmp/server</remotepath><matchname>*.txt</matchname><listfilename>/idc/data/ftplist/ftpgetfiles_surfdata.list</listfilename><ptype>1</ptype><remotepathbak>/tmp/idc/surfdatabak</remotepathbak><okfilename>/idcdata/ftplist/ftpgetfiles_surfdata.xml</okfilename>"
313行有问题=-=,现在已经解决,原来是早在167行xmltoarg时,由于copy了一些代码,所以一次性没改完,也就是getxmlbuffer函数的第三个参数我居然还是写的上一个starg.remotepathbak,真是大意了┭┮﹏┭┮
增量+修改下载文件 上面已经实现了,仅仅包括新增这种情况,对应应该修改的服务端、客户端文件目录,接下来,我们再包含,加上修改文件内容以后,我们应该考虑的目录情况
简而言之,程序的算法是一样的,但是需要把程序的时间考虑进去 5:50
修改结构体st_arg,加入bool checkmtime,修改帮助文档,修改解析xmltoarg,接着,跟着主函数一步一步看哪里需修改,第一处就是
第二处………………….将仅仅解析文件名称,变为解析时间和名称
1 /project/tools1/bin/ftpgetfiles /log/idc/ftpgetfiles_surfdata.log "<host>127.0.0.1:21</host><mode>1</mode><username>jjyaoao</username><password>gh</password><localpath>/tmp/client</localpath><remotepath>/tmp/server</remotepath><matchname>*.txt</matchname><listfilename>/idc/data/ftplist/ftpgetfiles_surfdata.list</listfilename><ptype>1</ptype><remotepathbak>/tmp/idc/surfdatabak</remotepathbak><okfilename>/idcdata/ftplist/ftpgetfiles_surfdata.xml</okfilename><checkmtime>true</checkmtime>"
于是现在又遇到了新的问题,=-=顺利解决,第一个就是上面这个框框最后那个checkmtime没有反斜线,最开始,被我日志debug找到咯
后面又遇到一个新的问题,他无法做到更新时间,始终要取得所有的txt文件,于是我发现我compare这个函数里面敲错了,=-=
最开始没加这个 == 0,不过好在已经发现,越来越理解项目了!
接下来,我们测试了true,理所应该也要测试测试false,因为这已经是最终版本了,最后测试显示,成功实现功能
1 2 3 4 5 6 7 8 9 10 11 具体测试步骤: mkdir /tmp/server touch /tmp/server/1.txt touch /tmp/server/2.txt touch /tmp/server/3.txt touch /tmp/server/4.txt touch /tmp/server/5.txt mkdir /tmp/client 第二次测试,,删除1.txt,新增6.txt,修改2.txt 即可看到结果,(checkmtime)true的话,会重新加载6和2 我们进log就可以看到 false的话,只会重新加载6
收尾工作 这个程序是一个网络的客户端 程序,这种程序一定会挂死,不知道什么时候挂死,所以一定要做进程的心跳
那些程序会挂死?
为了解决这个问题我们继续引入进程心跳机制……………….
我们上一章写的心跳,包括了超时时间,进程名,日志名,但是网络的超时时间难以估计,通常看网络状态,网络好就填小点,网络差就填久一点,也有可能同一个程序启动多个心跳的情况(多个文件下载的任务)
所以我们之前的运行参数结构体,还需要加上超时时间和进程名两个参数,接着改动需要加入这两个参数的地方,例如帮助文档,xmltoarg之类,最后扫描一遍项目流程,将可能会消耗时间超时的地方全部都updatetime,不用担心会不会超时的问题,因为本来就一个赋值语句,相比于没有记录到超时位置而言,显然这个浪费是可以接受的
最后调试运行,并且把多年前 那个调度 程序无法启动的bug 找到了=-=原因是之前配置clion的时候,因为有idc的存在,而导致无法编译(数据库相关还未配置)所以。。。,现在找到了就好啦!
四、文件上传的功能 知道了文件下载功能的实现之后,文件上传就变得十分简单。技术流程完全一样,只是有一些细节会发生变化
上传 / 下载的步骤对比
先把上次已成功上传的文件加载到容器一,然后用dir2获取本地的文件列表,得到容器二,再把容器二与容器一进行对比,不需要上传的文件放到容器三,需要上传的放到容器四
上传和下载的不同:
想要得到文件的目录,在下载,需要分三步走,上传,只需要dir一个指令即可
listfilename不需要了,remotepathbak改为localpathbak,checkmtime不需要了(检查服务端文件的时间)原因有如下
checkmtime里面while循环中有这句,意味着程序每运行一次,都需要把服务端目录中全部的时间取回来,如果服务端的目录很多,取时间这个动作要消耗大量的资源,包括客户端等待的时间,网络带宽,还有对服务端造成的压力,如果服务端中的目录不会更新,就应该把checkmtime设置为false,在文件下载的过程中,checkmtime的取值对性能和服务端的压力有很大的影响,但在上传的过程中checkmtime对程序的性能不会有任何的影响(没有任何代价),所以干脆就不要了
LoadLocalFile的openDir也有一些问题,他的缺省值是获取10000个文件
但这里使用的是我们自己的目录(本地目录),所以可以配置脚本来清理,一般就不会有这个问题
测试:
1 /project/tools1/bin/ftpputfiles /log/idc/ftpputfiles_surfdata.log "<host>127.0.0.1:21</host><mode>1</mode><username>jjyaoao</username><password>gh</password><localpath>/tmp/client</localpath><remotepath>/tmp/server</remotepath><matchname>*.txt</matchname><ptype>1</ptype><localpathbak>/tmp/idc/surfdatabak</localpathbak><okfilename>/idcdata/ftplist/ftpgetfiles_surfdata.xml</okfilename><timeout>80</timeout><pname>ftpgetfiles_surfdata</pname>"
给client 1 2 3 4 5.txt 然后开测就好
最后配置start.sh 和killall.sh脚本 我们就已经有三个主要的程序在运行了,生成数据程序,下载程序,上传程序
又一个权限小细节 好家伙!!不给权限就失败是吧!
给了秒OK,就TM离谱哦,下次果然还是用jjyaoao给root发信号吧!!!!!
学习总结
由于应用场景,每个业务系统的负责人的不同的,我们肯定不能在别人的业务系统上创建目录,这样很可能导致被人甩锅
在ftp服务端创建目录会影响效率
试探打开,不行就创建他,这些没什么代价
但在ftp服务端就不一样,每执行一次命令,就需要进行一次网络报文的传输
ftp.get()设置为false,是因为我们还有checkmtime存在,如果服务端的参数会改变,我们把checkmtime设置为true就可以实现重传
第三板块-基于TCP协议的文件传输系统
其实根本没咋过掌握,直接就是一个,裸开TCP!!!!冲冲冲
将socket的常用函数,进行封装,变成更佳好用的工具
速度非常快,比FTP快很多倍
一、socket基础知识学习
[Socket (一) 基础及接口函数 - Jack王 - 博客园 (cnblogs.com)](https://www.cnblogs.com/blogwww/p/9499811.html#:~:text=socket ()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。,connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。 客户端通过调用connect函数来建立与TCP服务器的连接。)
Socket,又称为套接字,Socket是计算机网络通信的基本的技术之一。如今大多数基于网络的软件,如浏览器,即时通讯工具甚至是P2P下载都是基于Socket实现的。本篇会介绍一下基于TCP/IP的Socket编程,并且如何写一个客户端/服务器程序。
1.1 背景介绍
Unix的输入输出(IO)系统遵循Open-Read-Write-Close这样的操作范本。当一个用户进程进行IO操作之前,它需要调用Open来指定并获取待操作文件或设备读取或写入的权限。一旦IO操作对象被打开,那么这个用户进程可以对这个对象进行一次或多次的读取或写入操作。Read操作用来从IO操作对象读取数据,并将数据传递给用户进程。Write操作用来将用户进程中的数据传递(写入)到IO操作对象。 当所有的Read和Write操作结束之后,用户进程需要调用Close来通知系统其完成对IO对象的使用。 在Unix开始支持进程间通信(InterProcess Communication,简称IPC)时,IPC的接口就设计得类似文件IO操作接口。在Unix中,一个进程会有一套可以进行读取写入的IO描述符。IO描述符可以是文件,设备或者是通信通道(socket套接字)。一个文件描述符由三部分组成:创建(打开socket),读取写入数据(接受和发送到socket)还有销毁(关闭socket)。 在Unix系统中,类BSD版本的IPC接口是作为TCP和UDP协议之上的一层进行实现的。消息的目的地使用socket地址来表示。一个socket地址是由网络地址和端口号组成的通信标识符。 进程间通信操作需要一对儿socket。进程间通信通过在一个进程中的一个socket与另一个进程中得另一个socket进行数据传输来完成。当一个消息执行发出后,这个消息在发送端的socket中处于排队状态,直到下层的网络协议将这些消息发送出去。当消息到达接收端的socket后,其也会处于排队状态,直到接收端的进程对这条消息进行了接收处理。
1.2 TCP和UDP通信 关于socket编程我们有两种通信协议可以进行选择。一种是数据报通信,另一种就是流通信。 1.2.1 数据报通信 数据报通信协议,就是我们常说的UDP(User Data Protocol 用户数据报协议)。UDP是一种无连接的协议,这就意味着我们每次发送数据报时,需要同时发送本机的socket描述符和接收端的socket描述符。因此,我们在每次通信时都需要发送额外的数据。 1.2.2 流通信 流通信协议,也叫做TCP(Transfer Control Protocol,传输控制协议)。和UDP不同,TCP是一种基于连接的协议。在使用流通信之前,我们必须在通信的一对儿socket之间建立连接。其中一个socket作为服务器进行监听连接请求。另一个则作为客户端进行连接请求。一旦两个socket建立好了连接,他们可以单向或双向进行数据传输。 我们进行socket编程使用UDP还是TCP呢。选择基于何种协议的socket编程取决于你的具体的客户端-服务器端程序的应用场景。下面我们简单分析一下TCP和UDP协议的区别 : 在UDP中,每次发送数据报时,需要附带上本机的socket描述符和接收端的socket描述符。而由于TCP是基于连接的协议,在通信的socket对之间需要在通信之前建立连接,因此会有建立连接这一耗时存在于TCP协议的socket编程。 在UDP中,数据报数据在大小上有64KB的限制。而TCP中也不存在这样的限制。一旦TCP通信的socket对建立了连接,他们之间的通信就类似IO流,所有的数据会按照接受时的顺序读取。 UDP是一种不可靠的协议,发送的数据报不一定会按照其发送顺序被接收端的socket接受。然后TCP是一种可靠的协议。接收端收到的包的顺序和包在发送端的顺序是一致的。 简而言之,TCP适合于诸如远程登录(rlogin,telnet)和文件传输(FTP)这类的网络服务。因为这些需要传输的数据的大小不确定。而UDP相比TCP更加简单轻量一些。UDP用来实现实时性较高或者丢包不重要的一些服务。在局域网中UDP的丢包率都相对比较低。
1.3 C 中的 Socket 编程
说白了Socket 是应用层与TCP/IP协议族通信的中间软件抽象层 ,它是一组接口 。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
在使用Socket API编程时,需要重点先了解几个API,包括:socket()、bind()、connect()、listen()、accept()、send()和recv()、sendto()和recvfrom()、close()和shutdown()、getpeername()、gethostname()。这些接口是在Winsock2.h 中定义的不是在 MFC 中定义的,只需包含 Winsock2.h 头文件和 Ws2_32.lib 库就可以了。
服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
1.3.1 客户端/服务端模式:
在TCP/IP网络应用中,通信的两个进程相互作用的主要模式是客户/服务器模式,即客户端向服务器发出请求,服务器接收请求后,提供相应的服务。 服务端 :建立socket,声明自身的端口号和地址并绑定到socket,使用listen打开监听,然后不断用accept去查看是否有连接,如果有,捕获socket,并通过recv获取消息的内容,通信完成后调用closeSocket关闭这个对应accept到的socket,如果不再需要等待任何客户端连接,那么用closeSocket关闭掉自身的socket。 客户端 :建立socket,通过端口号和地址确定目标服务器,使用Connect连接到服务器,send发送消息,等待处理,通信完成后调用closeSocket关闭socket。
1.3.2 编程步骤 (1)服务端 加载套接字库,创建套接字(WSAStartup()/socket()); 绑定套接字到一个IP地址和一个端口上(bind()); 将套接字设置为监听模式等待连接请求(listen()); 请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept()); 用返回的套接字和客户端进行通信(send()/recv()); 返回,等待另一个连接请求; 关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup()); (2)客户端 加载套接字库,创建套接字(WSAStartup()/socket()); 向服务器发出连接请求(connect()); 和服务器进行通信(send()/recv()); 关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup());
1.4 函数详解
1.4.1 socket()函数 int socket(int protofamily, int type, int protocol);//返回sockfd
sockfd是描述符。socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。 创建socket的时候,可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为: protofamily:即协议域,又称为协议族(family)。常用的协议族有,AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。 type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。 protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议(这个协议我将会单独开篇讨论!)。 注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。 当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
1.4.2 bind()函数 正如上面所说bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 函数的三个参数分别为: sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。 addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。 addrlen:对应的是地址的长度。 通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
1.4.3 listen()、connect()函数 如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。 int listen(int sockfd, int backlog); int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。 connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。
1.4.4 accept()函数 TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //返回连接connect_fd 参数sockfd 参数sockfd就是上面解释中的监听套接字,这个套接字用来监听一个端口,当有一个客户与服务器连接时,它使用这个一个端口号,而此时这个端口号正与这个套接字关联。当然客户不知道套接字这些细节,它只知道一个地址和一个端口号。 参数addr 这是一个结果参数,它用来接受一个返回值,这返回值指定客户端的地址,当然这个地址是通过某个地址结构来描述的,用户应该知道这一个什么样的地址结构。如果对客户的地址不感兴趣,那么可以把这个值设置为NULL。 参数len
它也是结果的参数,用来接受上述addr的结构的大小的,它指明addr结构所占有的字节个数。同样的,它也可以被设置为NULL。 如果accept成功返回,则服务器与客户已经正确建立连接了,此时服务器通过accept返回的套接字来完成与客户的通信。 注意: accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,这个套接字是连接套接字。 此时我们需要区分两种套接字, 监听套接字: 监听套接字正如accept的参数sockfd,它是监听套接字,在调用listen函数之后,是服务器开始调用socket()函数生成的,称为监听socket描述字(监听套接字) 连接套接字:一个套接字会从主动连接的套接字变身为一个监听套接字;而accept函数返回的是已连接socket描述字(一个连接套接字),它代表着一个网络已经存在的点点连接。
一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。 自然要问的是:为什么要有两种套接字?原因很简单,如果使用一个描述字的话,那么它的功能太多,使得使用很不直观,同时在内核确实产生了一个这样的新的描述字。 连接套接字socketfd_new 并没有占用新的端口与客户端通信,依然使用的是与监听套接字socketfd一样的端口号
1.4.5 read()、write()等函数 万事具备只欠东风,至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组: read()/write();recv()/send();readv()/writev();recvmsg()/sendmsg();recvfrom()/sendto()
int send( SOCKET s, const char FAR *buf, int len,int flags); 不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。 客户程序一般用send函数向服务器发送请求,而服务器则通常用send函数来向客户程序发送应答。 该函数的第一个参数指定发送端套接字描述符;第二个参数指明一个存放应用程序要发送数据的缓冲区;第三个参数指明实际要发送的数据的字节数;第四个参数一般置0。 int recv( SOCKET s, char FAR *buf, int len,int flags); 不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。 该函数的第一个参数指定接收端套接字描述符;第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;第三个参数指明buf的长度;第四个参数一般置0。
1.4.6 close()函数 在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。 #include <unistd.h> int close(int fd); close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。 注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
二、封装socket的API
解决了TCP报文粘包/分包的问题
封装socket的常用函数
粘包和分包
TCP Socket的粘包和分包的处理 - 不懂123 - 博客园 (cnblogs.com)
概述 在进行TCP Socket开发时,都需要处理数据包粘包和分包的情况.实际上解决该问题很简单,在应用层下,定义一个协议:消息头部+消息长度+消息正文即可。
分包和粘包 分包:发送方发送字符串”helloworld”,接收方却接收到了两个字符串”hello”和”world”
粘包:发送方发送两个字符串”hello”+”world”,接收方却一次性接收到了”helloworld”
socket环境有以上问题,但是TCP传输数据能保证几点:
\1. 顺序不变,例如发送方发送hello,接收方也一定顺序接收到hello,这个是TCP协议承诺的,因此这点成为我们解决分包,黏包问题的关键.
\2. 分割的包中间不会插入其他数据
因此如果要使用socket通信,就一定要自己定义一份协议.目前最常用的协议标准是:消息头部(包头)+消息长度+消息正文
TCP分包的原理 TCP 是以段 (Segment)为单位发送 数据的,建立TCP链接后,有一个最大消息长度(MSS).如果应用层数据包超过MSS ,就会把应用层数据包拆分 ,分成两个段来发送.
这个时候接收端的应用层 就要拼接 这两个TCP包,才能正确处理数据。
相关的,路由器 有一个MTU ( 最大传输单元)一般是1500 字节,除去IP头部20字节,留给TCP的就只有MTU-20字节。所以一般TCP的MSS 为MTU-20=1460 字节
当应用层数据超过1460 字节时,TCP会分多个数据包来发送 。
TCP粘包的原理 TCP为了提高网络的利用率,会使用一个叫做Nagle的算法.该算法是指,发送端 即使有要发送的数据 ,如果很少 的话,会延迟发送 .
如果应用层 给TCP传送数据很快 的话,就会把两个应用层数据包 “粘”在一起,TCP最后只发一个 TCP数据包给接收端.
小结 我们可以看见TCP发出去才会出现分包(接受数据那是应用层的事,我们默认应用层发送过来的都是完整数据呗),而粘包会出现在TCP接受数据和发送数据,分别是不同的原因
TCP的分包与粘包原理的简单理解
TCP的分包与粘包原理简单理解 - 掘金 (juejin.cn)
1,首先我们来看看粘包的图解:如下图:
为什么会出现粘包
假如说,我们要发送两个hello数据,一个hello占5个,TCP假如一次性传输能存10个。当第一个hello存进TCP的缓存区里面时,没有存满,还剩下5个空位,这时第二个hello过来,刚好占满剩下的5个,然后这两个hello就粘在一起了,变成hellohello了。
2,再来看看分包的图解:如下图:
为什么会出现分包
假如说,我们要发送两个hello,一个hello要占领5个空位。但是TCP的一个包只有4个空位,。这时第一个hello传过来,只存了hell,剩下的e被分到下一个包存储,所以就成了分包。
3,在哪种情况下会出现分包与粘包:
1,要发送 的数据大于 TCP发送缓冲区剩余空间大小 ,将会发生分包。
2,待发送 数据大于MSS (最大报文长度),TCP在传输前将进行分包。
3,要发送 的数据小于 TCP发送缓冲区的大小 ,TCP将多次写入 缓冲区的数据一次发送 出去,将会发生粘包。
4,接收数据 端的应用层****没 有及时读取 接收缓冲区中的数据,将发生粘包 。
自定义协议
解决粘包/分包常用方式:
两种方式
1,定义数据包包头,包头众包含数据完整包的长度,接收端接收到数据后,通过读取包头的长度字段,便知道每一个数据包的实际长度了。
比如说,将原数据加密,在密文前面加上包头,即:[包头]+[密文]。 包头=[密文长度+加密方式+…]
粘包实践 /project/public/socket中的demo03(客户端)和demo04(服务端)
TCP协议的保证 解决粘包方案
采用ASCII码 在实际开发一般不采用,因为有一个问题,当报文的内容超过四个9的时候,四个字节就存不下了,用整型变量存放报文长度就不会存在这个问题
采用整型
在开发框架中,tcpwrite和tcpread解决了这两个问题(粘包分包)
TcpWrite()/TcpRead() TcpWrite和TcpRead的使用一定要成双成对的,也就是协议需要大家一起来遵守
TcpWrite() 我们看这几行代码,注意我们发送缓冲区数据是采用的封装的Writen函数,而不是send(c自带),原因就是因为socket有缓冲区,读和写两个缓冲区,并且大小有限,如果这时候的写缓冲区快满了,还有五百字节可以用,调用send函数,只能成功写入500字节,剩下的要等缓冲区空闲了才能再次写入。Writen循环调用send函数,直到全部数据被成功的发送,返回true,如果发送过程中tcp断开了或者其他原因,返回false
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 bool TcpWrite (const int sockfd,const char *buffer,const int ibuflen) { if (sockfd==-1 ) return false ; int ilen=0 ; if (ibuflen==0 ) ilen=strlen (buffer); else ilen=ibuflen; int ilenn=htonl (ilen); char TBuffer[ilen+4 ]; memset (TBuffer,0 ,sizeof (TBuffer)); memcpy (TBuffer,&ilenn,4 ); memcpy (TBuffer+4 ,buffer,ilen); if (Writen (sockfd,TBuffer,ilen+4 ) == false ) return false ; return true ; }
Writen() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 bool Writen (const int sockfd,const char *buffer,const size_t n) { int nLeft=n; int idx=0 ; int nwritten; while (nLeft > 0 ) { if ( (nwritten=send (sockfd,buffer+idx,nLeft,0 )) <= 0 ) return false ; nLeft=nLeft-nwritten; idx=idx+nwritten; } return true ; }
TcpRead() 先不管超时时间,我们来梳理一下过程,把传进来的字节数初始化为0(注意用了解引用*)意思是我们把这个传进来的int型的地址里面的内容改为了0,接着使用Readn()接受sockfd中的内容,用ibuflen来当做容器接受好像不一定必须转为char,但反正recv底层接受的是void*,最后,在将报文长度的 网络字节序 用ntohl转换为主机字节序,最后在buffer接受得到报文的实际内容,同样用readn()函数,读sockfd里面的,读取的长度设置为ibuflen的大小
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 bool TcpRead (const int sockfd,char *buffer,int *ibuflen,const int itimeout) { if (sockfd==-1 ) return false ; if (itimeout>0 ) { struct pollfd fds ; fds.fd=sockfd; fds.events=POLLIN; if ( poll (&fds,1 ,itimeout*1000 ) <= 0 ) return false ; } if (itimeout==-1 ) { struct pollfd fds ; fds.fd=sockfd; fds.events=POLLIN; if ( poll (&fds,1 ,0 ) <= 0 ) return false ; } (*ibuflen) = 0 ; if (Readn (sockfd,(char *)ibuflen,4 ) == false ) return false ; (*ibuflen)=ntohl (*ibuflen); if (Readn (sockfd,buffer,(*ibuflen)) == false ) return false ; return true ; }
Readn() 函数反复调用recv,如果读取的过程中接收到n个子杰,函数返回true,如果读写的过程中发送了意外,比如说:连接被断开,那么就返回false
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 bool Readn (const int sockfd,char *buffer,const size_t n) { int nLeft=n; int idx=0 ; int nread; while (nLeft > 0 ) { if ( (nread=recv (sockfd,buffer+idx,nLeft,0 )) <= 0 ) return false ; idx=idx+nread; nLeft=nLeft-nread; } return true ; }
socket封装函数 我们先来看看一组对比:下面这个是没有用socket封装的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #include <stdio.h> #include <string.h> #include <unistd.h> #include <stdlib.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> int main (int argc,char *argv[]) { if (argc!=3 ) { printf ("Using:./demo01 ip port\nExample:./demo01 127.0.0.1 5005\n\n" ); return -1 ; } int sockfd; if ( (sockfd = socket (AF_INET,SOCK_STREAM,0 ))==-1 ) { perror ("socket" ); return -1 ; } struct hostent * h ; if ( (h = gethostbyname (argv[1 ])) == 0 ) { printf ("gethostbyname failed.\n" ); close (sockfd); return -1 ; } struct sockaddr_in servaddr ; memset (&servaddr,0 ,sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons (atoi (argv[2 ])); memcpy (&servaddr.sin_addr,h->h_addr,h->h_length); if (connect (sockfd, (struct sockaddr *)&servaddr,sizeof (servaddr)) != 0 ) { perror ("connect" ); close (sockfd); return -1 ; } int iret; char buffer[102400 ]; for (int ii=0 ;ii<10 ;ii++) { memset (buffer,0 ,sizeof (buffer)); sprintf (buffer,"这是第%d个超级女生,编号%03d。" ,ii+1 ,ii+1 ); if ( (iret=send (sockfd,buffer,strlen (buffer),0 ))<=0 ) { perror ("send" ); break ; } printf ("发送:%s\n" ,buffer); memset (buffer,0 ,sizeof (buffer)); if ( (iret=recv (sockfd,buffer,sizeof (buffer),0 ))<=0 ) { printf ("iret=%d\n" ,iret); break ; } printf ("接收:%s\n" ,buffer); sleep (1 ); } close (sockfd); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include "../_public.h" int main (int argc,char *argv[]) { if (argc!=3 ) { printf ("Using:./demo07 ip port\nExample:./demo07 127.0.0.1 5005\n\n" ); return -1 ; } CTcpClient TcpClient; if (TcpClient.ConnectToServer (argv[1 ],atoi (argv[2 ]))==false ) { printf ("TcpClient.ConnectToServer(%s,%s) failed.\n" ,argv[1 ],argv[2 ]); return -1 ; } char buffer[102400 ]; for (int ii=0 ;ii<100000 ;ii++) { SPRINTF (buffer,sizeof (buffer),"这是第%d个超级女生,编号%03d。" ,ii+1 ,ii+1 ); if (TcpClient.Write (buffer)==false ) break ; printf ("发送:%s\n" ,buffer); memset (buffer,0 ,sizeof (buffer)); if (TcpClient.Read (buffer)==false ) break ; printf ("接收:%s\n" ,buffer); sleep (1 ); } }
我们可以看到,用了socket,会让我们的程序设计变得简单,更多的精力可以用在业务逻辑的处理
CTcpClient() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class CTcpClient { public : int m_connfd; char m_ip[21 ]; int m_port; bool m_btimeout; int m_buflen; CTcpClient (); bool ConnectToServer (const char *ip,const int port) ; bool Read (char *buffer,const int itimeout=0 ) ; bool Write (const char *buffer,const int ibuflen=0 ) ; void Close () ; ~CTcpClient (); };
ConnectToServer() 这里有两个细节:
m_connfd是客户端是否已经连接的信号,如果都本身已经处于连接状态了,那就先关闭它,并且再把这个参数变为-1,也可以用false啥的,这个没啥特别的,但是一般连接状态的socket都大于0,所以用-1更容易区分。
重要 细节:SIGPIPE这个信号,在应用开发中,我们如果向已关闭的socket发出数据,内核就会发送SIGPIPE的信号,这个信号是直接关闭异常程序,我们肯定不希望直接就把他关闭了,所以一般来讲要屏蔽
1 2 3 4 5 6 7 8 9 10 11 bool CTcpClient::ConnectToServer (const char *ip,const int port) { if (m_connfd!=-1 ) { close (m_connfd); m_connfd=-1 ; } signal (SIGPIPE,SIG_IGN);
屏蔽与否SIGPIPE演示 可以看到仅第一条有输出
我们再把忽略信号加上…………..
Read() 程序也像我们打电话一样,如果打了十几秒,还没接,就可能会怀疑对方程序有问题,或者哪哪的问题,要挂掉,这里要用到io复用技术(poll),现在先暂时不了解,我们知道大致意思就好
就是如果itimeout>0就判断程序在itimeout指定的时间内有没有到达,没有的话把m_btimeout关掉,if的那段代码只判断有没有数据到达,不会读取数据,读取用return的TcpRead来读,TcpRead上面已经封装好了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 bool CTcpClient::Read (char *buffer,const int itimeout) { if (m_connfd==-1 ) return false ; if (itimeout>0 ) { struct pollfd fds ; fds.fd=m_connfd; fds.events=POLLIN; int iret; m_btimeout=false ; if ( (iret=poll (&fds,1 ,itimeout*1000 )) <= 0 ) { if (iret==0 ) m_btimeout = true ; return false ; } } m_buflen = 0 ; return (TcpRead (m_connfd,buffer,&m_buflen)); }
其余函数 Write中调用了TcpWrite,close关闭socket连接,在析构函数中调用了close
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 bool CTcpClient::Write (const char *buffer,const int ibuflen) { if (m_connfd==-1 ) return false ; int ilen=ibuflen; if (ibuflen==0 ) ilen=strlen (buffer); return (TcpWrite (m_connfd,buffer,ilen)); } void CTcpClient::Close () { if (m_connfd > 0 ) close (m_connfd); m_connfd=-1 ; memset (m_ip,0 ,sizeof (m_ip)); m_port=0 ; m_btimeout=false ; } CTcpClient::~CTcpClient () { Close (); }
CTcpServer() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 class CTcpServer { private : int m_socklen; struct sockaddr_in m_clientaddr ; struct sockaddr_in m_servaddr ; public : int m_listenfd; int m_connfd; bool m_btimeout; int m_buflen; CTcpServer (); bool InitServer (const unsigned int port,const int backlog=5 ) ; bool Accept () ; char *GetIP () ; bool Read (char *buffer,const int itimeout=0 ) ; bool Write (const char *buffer,const int ibuflen=0 ) ; void CloseListen () ; void CloseClient () ; ~CTcpServer (); };
InitServer() 三个细节:
忽略SIGPIPE信号
服务端一定要打开SO_REUSEADDR,否则下面的bind调用,很容易出现地址被使用的问题
传参列表 backlog,会填写在listen里面,在绝大多数情况下5已经够用了,如果客户端成百上千个,可以改大一点,50,100都行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 bool CTcpServer::InitServer (const unsigned int port,const int backlog) { if (m_listenfd > 0 ) { close (m_listenfd); m_listenfd=-1 ; } if ( (m_listenfd = socket (AF_INET,SOCK_STREAM,0 ))<=0 ) return false ; signal (SIGPIPE,SIG_IGN); int opt = 1 ; unsigned int len = sizeof (opt); setsockopt (m_listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,len); memset (&m_servaddr,0 ,sizeof (m_servaddr)); m_servaddr.sin_family = AF_INET; m_servaddr.sin_addr.s_addr = htonl (INADDR_ANY); m_servaddr.sin_port = htons (port); if (bind (m_listenfd,(struct sockaddr *)&m_servaddr,sizeof (m_servaddr)) != 0 ) { CloseListen (); return false ; } if (listen (m_listenfd,backlog) != 0 ) { CloseListen (); return false ; } return true ; }
关闭socket 有两个,一个用于关闭监听,一个用于关闭客户端的socket
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void CTcpServer::CloseListen () { if (m_listenfd > 0 ) { close (m_listenfd); m_listenfd=-1 ; } } void CTcpServer::CloseClient () { if (m_connfd > 0 ) { close (m_connfd); m_connfd=-1 ; } } CTcpServer::~CTcpServer () { CloseListen (); CloseClient (); }
网络字节序与主机字节序
https://www.cnblogs.com/xingguang1130/p/11643446.html
1、大端、小端字节序 考虑一个16位整数,它由2个字节组成。内存中存储这两个字节有两种方法:一种是将低序字节 存储在起始地址 ,这称为小端(little-endian)字节序;另一种方法是将高序字节 存储在起始地址 ,这称为大端(big-endian)字节序。如下所示:
术语“大端”和“小端”表示多个字节值的哪一端(小端或大端)存储在该值的起始地址。
遗憾 的是,这两种字节序之间没有标准可循 ,两种格式都有系统使用。比如,Inter x86、ARM核采用的是小端模式,Power PC、MIPS UNIX和HP-PA UNIX采用大端模式。
2、网络字节序和主机字节序 网络字节序
网络字节序 是TCP/IP中规定好 的一种数据表示格式 ,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节序采用big endian排序方式。 (大端—-big endian)
主机字节序
不同的机器 主机字节序不相同,与CPU 设计有关 ,数据的顺序 是由cpu决定 的,而与操作系统无关 。我们把某个给定系统所用的字节序称为主机字节序(host byte order)。比如x86 系列CPU 都是little-endian 的字节序。
由于这个原因不同体系结构 的机器之间无法通信 ,所以要转换成 一种约定的数序 ,也就是网络字节顺序。
网络字节序与主机字节序之间的转换函数:htons(), ntohs(), htons(),htonl(),位于头文件<netinet/in.h>,htons和ntohs完成16位无符号数的相互转换,htonl和ntohl完成32位无符号数的相互转换。
在使用little endian 的系统中,这些函数会把字节序进行转换 ;
在使用big endian 类型的系统 中,这些函数会定义成空宏 ;
在网络程序开发 时 或是跨平台开发 时,也应该注意保证只用一种字节序 ,不然两方的解释不一样就会产生bug。
3、IP地址的三种表示格式及在开发中的应用 1)点分十进制表示格式
2)网络字节序格式
3)主机字节序格式
用IP地址127.0.0.1为例:(我估计这个例子是把小端主机转化为大端网络字节序)
第一步 127 . 0 . 0 . 1 把IP地址每一部分转换为8位的二进制数。
第二步 01111111 00000000 00000000 00000001 = 2130706433 (主机字节序)
然后把上面的四部分二进制数从右往左按部分重新排列,那就变为:
第三步 00000001 00000000 00000000 01111111 = 16777343 (网络字节序)
eg:
struct sockaddr_in addrSrv;
1 2 3 4 addrSrv.sin_addr.S_un.S_addr = inet_addr ("127.0.0.1" ); addrSrv.sin_family=AF_INET; addrSrv.sin_port=htons (6000 );
4、inet_aton()、inet_addr()和inet_ntoa()函数 头文件: <arpa/inet.h>
1)int inet_aton(const char *strptr, struct in_addr *addrptr);
将strptr所指C字符串转换 成一个32位的网络字节序 二进制值,并通过指针addrptr来存储。若成功则返回1,否则返回0。
2)in_addr_t inet_addr(const char *strptr)
若字符串有效,则返回值 为32位 的网络字节序 二进制值,否则为INADDR_NONE 。
该函数存在一个问题,所有2^32^个可能的二进制值都是有效的IP地址(0.0.0.0—255.255.255.255),但是当出错时该函数返回INADDR_NONE 常值(通常是一个32位均为1 的值)。这意味着点分十进制数串255.255.255.255 ** 不能 由该函数 处理,因为它的二进制值用来指示该函数失败。所以该函数已经被废弃, 应该尽量用inet_aton()函数**,或者将要说到的inet_pton()函数。
3)char *inet_ntoa(struct in_addr inaddr);
将一个32位的网络字节序二进制值IPv4地址转换成相应的点分十进制数串。该函数以一个结构而不是以指向该结构的一个指针作为其参数。
返回:指向一个点分十进制数串的指针
5、inet_pton()和inet_ntop()函数
这两个函数是随着IPv6出现的新函数,对于IPv4地址和IPv6地址都适用。函数名中p和n分别代表表达(presentation)和数值(numeric)。
头文件: <arpa/inet.h>
总结这几个转换函数:
C语言-网络/主机字节序 如何判断字节序 字节序的判断只需要一段简单的代码即可:
1 2 3 4 5 6 7 8 9 int main() { int x=0x12345678; unsigned char *p=(char *)&x; printf("%p %p %p %p\r\n",p,p+1,p+2,p+3); printf("%0x %0x %0x %0x\r\n",p[0],p[1],p[2],p[3]); } 12345678
在ubuntu下的执行结果为:
从执行结果可以得到低字节的78存储在低地址0x7ffe4970678c,所以ubuntu的字节序是小端序。
linux下还可以使用shell查看字节序
1 2 3 lscpu | grep -i byte 12
lscpu表示查看cup的相关信息,grep -i byte表示过滤字节序字段。
二、网络字节序 TCP/IP协议规定:把接收到的第一个字节当作高位字节看待,所以网络字节序是大端序; 我们的电脑和一些常用的处理器芯片大都是小端序的存储方式,在发送数据之前需要进行字节序的转换。
假设我们需要把0x12345678通过udp发送出去,我们在linux上写如下代码:
1 2 3 int buffer = 0x12345678; ret = sendto(sockfd,&buffer,sizeof(int),0,&dest_addr,sizeof(dest_addr)); 12
通过wireshark抓取得到的数据包为:
我们可以使用htonl将主机字节序转换成网络字节序:
1 2 3 4 int buffer = 0x12345678; buffer = htonl(buffer); ret = sendto(sockfd,&buffer,sizeof(int),0,&dest_addr,sizeof(dest_addr)); 123
使用wireshark抓取得到的数据如下:
ipv4/ipv6
IPv4和IPv6有什么区别? - 知乎 (zhihu.com)
Internet协议(IP)是为连接到Internet网络的每个设备分配的数字地址。它类似于电话号码,是一种独特的数字组合,允许用户与他人通信。IP地址主要有两个主要功能。首先,有了IP,用户能够在Internet上被识别 。第二,IP地址允许计算机通过Internet发送和接收数据 ,也就是我们经常说的通信。在本文中,我们将深入研究两种类型的IP地址:IPv4与IPv6。我们将从以下几个方面来给大家介绍这两种类型的IP地址,让大家了解到两类IP的必备知识:
什么是IPv4和IPv6?
IPv4和IPv6之间的区别
IPv4或IPv6:使用哪个?
IPv4与IPv6安全性如何
什么是IPv4和IPv6? IPv4和IPv6是不同类型的IP地址。它们的主要用途相同,标记不同的用户,并且让用户能通过IP进行通信。主要区别在于IPv6是最新一代的IP地址。
IPv4地址
IPv4地址的概念是在1980年代初期提出的。即使有新版本的IP地址,IPv4地址仍然是Internet用户使用最广泛的地址。通常,IPv4地址以点分十进制 表示。每个部分代表一组构成8位地址方案的8位地址。
IPv4地址组合的数量是有限的。总体而言,可以算出40亿(256 4) 个唯一地址。在IPv4地址才开始时,这个数字似乎永远不会过期。但是,现在情况有所不同了。2011年,全球互联网编号分配机构(IANA)分发了IPv4地址空间的最后一块。2015年,IANA正式宣布美国已用完IPv4地址。直到今天,IPv4地址仍然承载着最多(超过90%)的互联网流量。到目前为止,即使目前存在IPv4地址耗尽的问题,也有一些方法可以继续使用IPv4地址。例如,当仅需要一个唯一的IP地址来代表一组设备时,网络地址转换(NAT)是一种方法。除此之外,IP地址可以重复使用。当然,我们已经有了彻底耗尽的解决方案-IPv6地址。
IPv6地址
仔细观察,您会发现IPv6地址并不是一种全新的技术。它是Internet协议的最新版本,但它是在1998年开发的,旨在替换IPv4地址。IPv6地址使用以冒号分隔的十六进制数字。它分为八个16位块,构成一个128位地址方案。
IPv6也存在数量限制。不过可用的IP数量远大于IPv4。从理论上讲,可以创建大约3.4×10 38 个地址。这一数据听起来很高,远超于IPv4的总数40亿个,但是有一天也可能出现不够的情况。但就目前而言,这些地址将可以供我们使用很长一段时间。
IPv4和IPv6之间的区别 IPv4和IPv6用于用户标识和Internet上不同设备之间的通信。IPv4是32位IP地址,而IPv6是128位IP地址。IPv4是数字地址,用点分隔。IPv6是一个字母数字地址,用冒号分隔。
我们分别详细介绍了IPv4和IPv6类型。现在,我们可以比较这些类型,并找出这两种协议之间的主要区别。我们列举了IPv4和IPv6之间的八个主要区别。
1.地址类型。 IPv4具有三种不同类型的地址:多播,广播和单播。IPv6还具有三种不同类型的地址:任意广播,单播和多播。
2.数据包大小。 对于IPv4,最小数据包大小为576字节。对于IPv6,最小数据包大小为1208字节。
3.header区域字段数。 IPv4具有12个标头字段,而IPv6支持8个标头字段。
4.可选字段。 IPv4具有可选字段,而IPv6没有。但是,IPv6具有扩展header,可以在将来扩展协议而不会影响主包结构。
5.配置。 在IPv4中,新装的系统必须配置好才能与其他系统通信。在IPv6中,配置是可选的,它允许根据所需功能进行选择。
6.安全性。 在IPv4中,安全性主要取决于网站和应用程序。它不是针对安全性而开发的IP协议。而IPv6集成了Internet协议安全标准(IPSec)。IPv6的网络安全不像IPv4是可选项,IPv6里的网络安全项是强制性的。
7.与移动设备的兼容性。 IPv4不适合移动网络,因为正如我们前面提到的,它使用点分十进制表示法,而IPv6使用冒号,是移动设备的更好选择。
8.主要功能。 IPv6允许直接寻址,因为存在大量可能的地址。但是,IPv4已经广泛传播并得到许多设备的支持,这使其更易于使用。
IPv4或IPv6:使用哪个? 对于使用IPv6还是IPv4这个问题,没有标准答案。在考虑未来的网络体验时,IPv6地址就显得至关重要。即使在我们已经没有网络地址的情况下仍然可以有其他办法使用IPv4地址,但是这些选项也可能会轻微影响到网络速度或引起其他问题。不过,使用IPv6需要开发支持IPv6的新技术和产品。IPv6的速度显然不比IPv4快,但是从IPv4完全更改为IPv6将为Internet提供更大的唯一IP池。那么为什么我们仍在使用IPv4?
问题就在于IPv4和IPv6无法相互通信。这就是为什么IPv6的集成和适配很复杂。大多数网站或应用程序仅支持IPv4类型的IP地址。想象一下突然更改每个设备的IP地址。用户将无法访问大多数网站或应用程序,而我们在互联网上将陷入一片混乱。从旧的IP类型转换为新的IP类型的过程应分步完成。例如,这两个协议能够并行运行。此功能称为双重堆栈 。它允许用户同时访问IPv4和IPv6内容。
您需要什么才能使用IPv6?
1.操作系统必须与IPv6兼容。Windows Vista和Windows的较新版本,Mac OS X的现代版本以及Linux。
2.大多数路由器不支持IPv6。如果您想尝试使用IPv6,请检查路由器的详细信息。
\3. Internet服务提供商(ISP)也必须支持IPv6。即使您具有合适的操作系统和路由器,您的ISP也必须提供IPv6连接。
IPv4与IPv6的安全性 IPv6的开发考虑了安全性 。这就是将IPSec集成在IPv6中的原因,而对于IPv4,IPSec是可选的。
什么是IPSec?
IPSec(Internet协议安全性) 是一种安全的网络协议,它对数据包进行身份验证和加密,以在设备之间提供安全的通信。加密是只有经过确认的各方才能理解的一种秘密代码。它有助于确保通过公共网络发送的信息的安全。
由于IPv4还可以选择集成IPSec,因此我们可以假设在安全性方面IPv4与IPv6几乎相同。但是,如果已经集成了安全措施,则要简单得 多。
三、多进程的网络服务端
服务端可以是多进程,也可以是多线程,如果采用IO复用技术,单进程单线程的服务端也可以和多个客户端程序通信,这个章节我们先搞定定多进程和多线程的服务端
主体流程 父进程先初始化服务端,然后Accept等待服务端的连接,新的客户端连上了之后,fork一个子进程出来,然后父进程回到accept继续等待其他客户端的连接请求,让子进程与刚才连进来的客户端进行处理业务。
基础实现 寄托于fork函数实现,我们用while反复迭代,达到不断接受客户端的目的(回到Accept),
但接下来就出现了一个问题(demo10为服务端),现在,客户端已经全部结束了,但是服务端还有这么多进程,这是怎么回事呢?
那原因就在那个while,当fork生成子进程,子进程进入while,并且执行完自己的程序之后,就会继续进入上面那层while,并且不断卡在连接阶段,就和父进程一起等待在哪里挂起了
所以我们应该在最下面增加一行代码,用return 0或者exit(0)都可以
再优化 现在,我们解决了服务端进程等待的问题,接下来,新的问题又出现了。客户端全部退出之后,服务端的进程变成了这样,也就是产生了很多僵尸进程。因为最后服务端还没有关闭的,至少最初的父进程任然在等待子进程,所以说哪些进程暂时挂起,变成僵尸进程,占用进程号
新的问题
我们在程序连接到,并且不做任何行动的这段时间内,sleep100s,
现在我们得到demo10的进程编号,
在fd里,有他打开过的文件描述符
0,1,2是标准输入,标准输出,标准错误,3应该是监听的socket,4
现在我们在换个花样,如果我们把sleep放在fork之后,让一个客户端连上去。
此时我们可以观察到
父进程和子进程文件表示符一样的!因为fork那节课,就已经讲过,fork是全部复制,也就是二者的任何信息,不会和彼此有关联,是独立的个体。
在网络服务程序中,父进程只负责监听客户端的连接,客户端连上了之后,对父进程来说,connfd是不需要的,对子进程而言,他使用的只是connfd,他也不需要监听的listenfd,既然这样,那么我们可以在父进程中关掉4,子进程中关掉3
当然,不关闭这两个文件描述符,其实也可以,但是对一个进程来说,打开的文件描述符是有限制的,打开的越多,消耗的资源会更多,所以我们肯定会执行这两行的,再开发当中
另外,日志闪亮登场。(将服务端的printf语句改为file.Write(argv[2], a+) == false…..)
fork一点点补充: 至于fork()函数的返回值: 子进程返回:0 父进程返回:>0的整数(返回子进程ID号) 错误返回:-1
补充这个的目的就是说,我们可以完全看出子进程继续往下,父进程再度回去(用continue,等效回去,而且本来就是第一个进程,所以自然也是父进程)
多进程网络服务程序(端)的退出 我们之前知道,可以用信号(killall xx xxx)来实现单进程的程序退出(杀掉),我们现在来思考一下多进程该如何退出,应该说,什么才是我们想要的退出,我们知道,对于一个进程而言,当他的父进程退出了,他不会跟着一起退出,所以说:
如果杀掉父亲,进行中的子进程不会跟着一起退出,会执行到结束,当子进程结束了,就别让他挂起了,喊他退出,但在这个期间内,新的客户端想要连接客户端,就不会被通过,因为父进程是用于监听的,连监听的都死掉了,你还怎么连进来?
如果杀掉孩子,我们自然是不希望影响到别的进程的,但是,我们也不想让这个孩子变成僵尸进程,所以,我们最开始的想法是让父进程忽略掉子进程的信号,现在我们可以把它封装成一个chldEXIT函数,具体思路如下面代码
如果父子都收到了信号,叫你们断掉,那么和第一种情况类似
1 2 3 4 5 6 7 8 9 * 1 )在多进程的服务程序中,如果杀掉一个子进程,和这个子进程通讯的客户端会断开,但是,不 * 会影响其它的子进程和客户端,也不会影响父进程。 * 2 )如果杀掉父进程,不会影响正在通讯中的子进程,但是,新的客户端无法建立连接。 * 3 )如果用killall+程序名,可以杀掉父进程和全部的子进程。 * * 多进程网络服务端程序退出的三种情况: * 1 )如果是子进程收到退出信号,该子进程断开与客户端连接的socket,然后退出。 * 2 )如果是父进程收到退出信号,父进程先关闭监听的socket,然后向全部的子进程发出退出信号。 * 3 )如果父子进程都收到退出信号,本质上与第2 种情况相同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void FathEXIT (int sig) { logfile.Write ("父进程退出,sig=%d。\n" ,sig); TcpServer.CloseListen (); kill (0 ,15 ); exit (0 ); } void ChldEXIT (int sig) { logfile.Write ("子进程退出,sig=%d。\n" ,sig); TcpServer.CloseClient (); exit (0 ); }
问题再深入 当服务端客户端正常运行的时候,如果我们把服务端突然来个ctrl终止了,他就GG了,但是我们现在观察后台
他居然退出了两次,我们说,这不是我们想要的(虽然他实质肯定就退出了一次)
这样的原因是因为,信号退出处理函数在执行的过程中又受到了信号,要解决这个问题,我们可以,给退出函数,做屏蔽处理,具体如下哦,这样无敌的退出函数就出来啦
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void FathEXIT (int sig) { signal (SIGINT,SIG_IGN); signal (SIGTERM,SIG_IGN); logfile.Write ("父进程退出,sig=%d。\n" ,sig); TcpServer.CloseListen (); kill (0 ,15 ); exit (0 ); } void ChldEXIT (int sig) { signal (SIGINT,SIG_IGN); signal (SIGTERM,SIG_IGN); logfile.Write ("子进程退出,sig=%d。\n" ,sig); TcpServer.CloseClient (); exit (0 ); }
讨论 思路别限制的太死,退出函数除了响应退出信号,还可以在其他的地方调用它,比如说,这个return,我们就可以改成FathEXIT(-1)
又或者是这里,我们处理结束以后把客户端关了,可以用我们刚刚写好的ChldEXIT(0)
kill() 头文件:#include <sys/types.h> #include <signal.h>
定义函数:int kill(pid_t pid, int sig);
函数说明:kill()可以用来送参数sig 指定的信号给参数pid 指定的进程。参数pid 有几种情况:1 、pid>0 将信号传给进程识别码为pid 的进程.2 、pid=0 将信号传给和目前进程相同进程组的所有进程3 、pid=-1 将信号广播传送给系统内所有的进程4 、pid<0 将信号传给进程组识别码为pid 绝对值的所有进程参数 sig 代表的信号编号可参考附录D
返回值:执行成功则返回0, 如果有错误则返回-1.
错误代码: 1、EINVAL 参数sig 不合法 2、ESRCH 参数pid 所指定的进程或进程组不存在 3、EPERM 权限不够无法传送信号给指定进程
网银APP软件业务 网络通信 首先,我们在介绍之前,先来了解网络通信的过程
客户端发起连接请求,服务端响应,建立连接,他们之间就可以进行通信了
请求报文可以由客户端发起,也可以由服务端发起,并且,请求报文和回应报文之间的关系,可以是一对一,也可以是多对多,一对多之类的
总的来说,没有固定的格式,要看双方的约定,所谓的约定就是通信协议,结束以后,可以由客户端断开,也可以由服务端断开
业务实例 登录
银行的服务端,收到报文之后,先判断业务代码,如果是1,表示登录业务,再判断手机号码和密码,如果号码和密码都正确,服务端返回0,提示成功,不正确….
我的账户
客户端 我们先从客户端改起,把登录业务和查看余额,封装成两个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include "../_public.h" CTcpClient TcpClient; bool srv001 () ; bool srv002 () ; int main (int argc,char *argv[]) { if (argc!=3 ) { printf ("Using:./demo11 ip port\nExample:./demo11 127.0.0.1 5005\n\n" ); return -1 ; } if (TcpClient.ConnectToServer (argv[1 ],atoi (argv[2 ]))==false ) { printf ("TcpClient.ConnectToServer(%s,%s) failed.\n" ,argv[1 ],argv[2 ]); return -1 ; } if (srv001 ()==false ) { printf ("srv001() failed.\n" ); return -1 ; } if (srv002 ()==false ) { printf ("srv002() failed.\n" ); return -1 ; } return 0 ; }
登录业务 三步走,先发送请求,再接受回报,最后解析回报
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 bool srv001 () { char buffer[1024 ]; SPRINTF (buffer,sizeof (buffer),"<srvcode>1</srvcode><tel>1392220000</tel><password>123456</password>" ); printf ("发送:%s\n" ,buffer); if (TcpClient.Write (buffer)==false ) return false ; memset (buffer,0 ,sizeof (buffer)); if (TcpClient.Read (buffer)==false ) return false ; printf ("接收:%s\n" ,buffer); int iretcode=-1 ; GetXMLBuffer (buffer,"retcode" ,&iretcode); if (iretcode!=0 ) { printf ("登录失败。\n" ); return false ; } printf ("登录成功。\n" ); return true ; }
查询业务 同样三步走,发送请求,接受回报,解析回报
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 bool srv002 () { char buffer[1024 ]; SPRINTF (buffer,sizeof (buffer),"<srvcode>2</srvcode><cardid>62620000000001</cardid>" ); printf ("发送:%s\n" ,buffer); if (TcpClient.Write (buffer)==false ) return false ; memset (buffer,0 ,sizeof (buffer)); if (TcpClient.Read (buffer)==false ) return false ; printf ("接收:%s\n" ,buffer); int iretcode=-1 ; GetXMLBuffer (buffer,"retcode" ,&iretcode); if (iretcode!=0 ) { printf ("查询余额失败。\n" ); return false ; } double ye=0 ; GetXMLBuffer (buffer,"ye" ,&ye); printf ("查询余额成功(%.2f)。\n" ,ye); return true ; }
服务端 在接受客户端和发送响应之间插入处理业务的主函数
解析+处理,用switch,判断他的isrvcode来实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 bool _main(const char *strrecvbuffer,char *strsendbuffer){ int isrvcode=-1 ; GetXMLBuffer (strrecvbuffer,"srvcode" ,&isrvcode); if ( (isrvcode!=1 ) && (bsession==false ) ) { strcpy (strsendbuffer,"<retcode>-1</retcode><message>用户未登录。</message>" ); return true ; } switch (isrvcode) { case 1 : srv001 (strrecvbuffer,strsendbuffer); break ; case 2 : srv002 (strrecvbuffer,strsendbuffer); break ; case 3 : srv003 (strrecvbuffer,strsendbuffer); break ; default : logfile.Write ("业务代码不合法:%s\n" ,strrecvbuffer); return false ; } return true ; }
登录业务 这里仅用登录业务就好,大同小异,都是进一步解析,因为上一步直解析了isrvcode,这里解析更细致的,登录所需要的参数解析在这,再处理业务,最后strsendbuffer发送出去,其他也是一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 bool srv001 (const char *strrecvbuffer,char *strsendbuffer) { char tel[21 ],password[31 ]; GetXMLBuffer (strrecvbuffer,"tel" ,tel,20 ); GetXMLBuffer (strrecvbuffer,"password" ,password,30 ); if ( (strcmp (tel,"1392220000" )==0 ) && (strcmp (password,"123456" )==0 ) ) { strcpy (strsendbuffer,"<retcode>0</retcode><message>成功。</message>" ); bsession=true ; } else strcpy (strsendbuffer,"<retcode>-1</retcode><message>失败。</message>" ); return true ; }
另外,我们在思考一下,现在服务程序使用了switch case 所以登录和其他业务是并列关系,但是,我们可以知道,对于一个网银系统而言,你连登录都没登录,肯定是不能执行下面的语句的,因此,我们还得做一些修改
在服务端,我们定义一个bession变量
并且在处理业务的函数中,switch判断之前,加一个检测到未登录,就退出,具体就是说,如果他的isrvcode不是1,(不是进行登录业务)那么我们检测一下他的bsession打开没有,bsession默认没有打开 ,没有打开说明没有登录 ,就让他退出,为了实现这个功能,我们就在登录成功的业务代码哪里加上 bsession = true ,这样就可以说,每一个客户端的子进程,最开始bsession都是false,实现了登录业务后,就永久打开,直到他退出!
Tcp长连接,短连接
[TCP的长连接和短连接-阿里云开发者社区 (aliyun.com)](https://developer.aliyun.com/article/37987#:~:text=长连接与短连接 . 所谓长连接,指在一个TCP连接上可以连续发送多个数据包,在TCP连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接,一般需要自己做在线维持。.,短连接是指通信双方有数据交互时,就建立一个TCP连接,数据发送完成后,则断开此TCP连接,一般银行都使用短连接。. 比如http的,只是连接、请求、关闭,过程时间较短%2C服务器若是一段时间内没有收到请求即可关闭连接。. 其实长连接是相对于通常的短连接而说的,也就是长时间保持客户端与服务端的连接状态。.)
简介: TCP/IP是个协议组,可分为三个层次:网络层、传输层和应用层。 在网络层有IP协议、ICMP协议、ARP协议、RARP协议和BOOTP协议。 在传输层中有TCP协议与UDP协议。在应用层有FTP、HTTP、TELNET、SMTP、DNS等协议。
TCP/IP是个协议组,可分为三个层次:网络层、传输层和应用层。
在网络层有IP协议、ICMP协议、ARP协议、RARP协议和BOOTP协议。 在传输层中有TCP协议与UDP协议。 在应用层有FTP、HTTP、TELNET、SMTP、DNS等协议。
长连接与短连接 所谓长连接,指在一个TCP连接上可以连续发送多个数据包,在TCP连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接,一般需要自己做在线维持。
短连接是指通信双方有数据交互时,就建立一个TCP连接,数据发送完成后,则断开此TCP连接,一般银行都使用短连接。
比如http的,只是连接、请求、关闭,过程时间较短,服务器若是一段时间内没有收到请求即可关闭连接。
其实长连接是相对于通常的短连接而说的,也就是长时间保持客户端与服务端的连接状态。
长连接与短连接的操作过程 通常的短连接操作步骤是: 连接→数据传输→关闭连接;
而长连接通常就是: 连接→数据传输→保持连接(心跳)→数据传输→保持连接(心跳)→……→关闭连接;
这就要求长连接在没有数据通信时,定时发送数据包(心跳),以维持连接状态,短连接在没有数据传输时直接关闭就行了。
什么时候用长连接,短连接 长连接可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间。对于频繁请求资源的客户来说,较适用长连接。不过这里存在一个问题,存活功能的探测周期太长,还有就是它只是探测TCP连接的存活,属于比较斯文的做法,遇到恶意的连接时,保活功能就不够使了。在长连接的应用场景下,client端一般不会主动关闭它们之间的连接,Client与server之间的连接如果一直不关闭的话,会存在一个问题,随着客户端连接越来越多,server早晚有扛不住的时候,这时候server端需要采取一些策略,如关闭一些长时间没有读写事件发生的连接,这样可 以避免一些恶意连接导致server端服务受损;如果条件再允许就可以以客户端机器为颗粒度,限制每个客户端的最大长连接数。
短连接对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段。但如果客户请求频繁,将在TCP的建立和关闭操作上浪费时间和带宽。
长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。
而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧 。所以并发量大,但每个用户无需频繁操作情况下需用短连接好。
总之,长连接和短连接的选择要视情况而定。
Tcp短连接 一对一的请求与回应,快速+高频,使用资源很多,使用的场景不多
连接之后,马上进行通讯,通讯结束之后,连接就断开了,管理起来非常简单,不需要其他的控制手段
Tcp长连接 大部分场景,采用Tcp长连接,这个过程就是,通信的次数和时间,是不确定的
n秒之后,服务端也可能主动发起连接请求 …….
比如说我们用vx给好友发消息,vx登录之后,我们的手机和腾讯的服务器建立了Tcp连接,服务端在把信息转发给你,服务端向客户端转发的时候,就是服务端主动连接你,vx业务采用的是Tcp长连接,要求客户端一直在线,否则满足不了业务需求,如果你不在线,就收不到别人给你发的信息,很自然
长连接和短连接的退出就有很大的区别,连接建立之后,除非程序退出,或者网络断开,否则,这个连接会一直保存,这样的话就产生了一个新的问题,如何管理Tcp连接? 方法是这样的,采用Tcp心跳机制
Tcp长连接心跳机制 理解了之后,要实现就很容易,一句话,客户端在空闲的时候,要向服务端,发送心跳报文。
心跳报文的格式也很简单
我们就从网银系统开始改,想一下需要做什么,首先我们得在服务端传参列表,加入心跳一项(自己拟定多少秒算超时),心跳传进来,他也算是一种业务的类型,我们可以放在switch里面 接着,假设我们心跳的业务处理代码为0,我们就可以整一个方法,叫做bool srv000,解析 下xml的心跳一项,其实吧,说是解析,根本不需要解析,只要客户端在规定时间传进来了,不就证明他没死吗,所以只会成功,不会失败
接着我们改客户端,让他具有发送的能力,并且,只要客户端有收到服务端的报文,我们就可以认为他是成功的,没必要解析xml,如果报文发不出去,或者没有收到回应,我们就说,他是失败的
我们现在来测试一下,注意,我们先设置服务端最大空闲时间是11秒,也就是说,虽然这里sleep了两次,加起来超过了10秒,但是我们心跳是针对于,不作任何处理的时间段,所以就没有这个顾虑。注意 :为什么说没有顾虑呢?,因为在服务端的switch语句里面,第一行就是先执行心跳语句,也就是说,无论我们执行了什么操作,首先都能进入心跳判断那一行,更新了心跳之后,再执行对应的语句,所以说,每执行一种业务,都刷新一次心跳,具体判断,我们是用的read里面封装的心跳机制,应该用到了IO复用的技术,以后遇到了在研究
我们先来运行服务端
再来运行客户端
如果把超时时间改成8
从结果来说,是非常顺利的!
应用经验
如果我们有连接云服务器之类的经验,就知道Tcp空闲的时候会被断开,中间经过了很多防火墙路由器什么的
例如,在这里,就设置了每50s发送一次心跳,这样的话一天也不会断开,所以,在项目开发中
太短没有必要,太长也不合适,肯定要比网络设备之间要短,一般60s
四、基于Tcp协议的文件传输系统 从这里开始,正式开始文件传输系统的实现!
之前开发的Ftp文件传输系统,主要用于系统之间,文件传输交换,Ftp很简单,但是效率不高;基于Tcp的主要用于系统内部的代码交换,代码写起来麻烦一些,但是文件传输的效率特别高,功能也更强大
这是我们的框架,为什么考虑分成三个部分,这能让系统的结构更佳简单,可能这里会产生一个疑惑,为什么客户端分成上传下载,服务端则不分呢?
原因是这样的,服务端是网络服务程序,两个网络服务程序,就需要两个监听的端口,这样的话配置网络参数会更麻烦,比如路由器,要开通两个端口,防火墙也要开通两个端口
文件上传功能 流程图:
首先,登录的意义并不是判断用户名和密码,而是与服务端协商文件传输的参数,最重要的参数是文件存放的目录
文件信息:文件名,文件时间,文件大小
文件内容:里面存放的内容
服务端接受文件之后,再向客户端发送报文,客户端收到回复的报文,就算上传成功了,然后用while循环来执行这一段迭代,保证把文件清单里每一个文件都上传,当上传完毕,客户端可以休息几秒,再去获取文件清单,再把文件上传给服务端,按照上述的步骤循环。
要求:
第一个要求是因为,该传输功能处于系统内部,对文件传输的时效要求很高,不能延迟太长时间
第二个要求是客户端不需要增量上传的功能,上传以后删除便是,这种处理方法最简单,效率也最高,当然,如果要同时上传到多个服务端,那我们可以上传的时候多拷贝几份嘛
exit与return 在实现的过程中我们用到了return和exit的关系,所以在这里我又去百度了一下…
exit(0):正常运行程序并退出程序;
exit(1):非正常运行导致退出程序;
return():返回函数,若在主函数中,则会退出函数并返回一值。
详细说:
return返回函数值 ,是关键字; exit 是一个函数 。
return 是语言 级别的,它表示了调用堆栈的返回;而exit 是系统 调用级别的,它表示了一个进程的结束。
return是函数的退出 (返回);exit是进程的退出 。
return是C语言提供的,exit是操作系统提供的(或者函数库中给出的)。
return用于结束一个函数的执行,将函数的执行信息传出个其他调用函数使用;exit 函数是退出应用程序,删除进程使用的内存空间 ,并将应用程序的一个状态返回给OS ,这个状态标识了 应用程序的一些运行信息 ,这个信息和机器和操作系统有关,一般是 0 为正常退出, 非0 为非正常退出。
非主函数中调用return和exit效果很明显,但是在main函数中调用return和exit的现象就很模糊,多数情况下现象都是一致的。
exit(0)与exit(1)对你的程序来说,没有区别。对使用你的程序的人或者程序来说,区别可就大了。 一般来说,exit 0 可以告知你的程序的使用者:你的程序是正常结束的。如果 exit 非 0 值,那么你的程序的使用者通常会认为你的程序产生了一个错误。
以 shell 为例,在 shell 中调用完你的程序之后,用 echo $? 命令就可以看到你的程序的 exit 值。在 shell 脚本中,通常会根据上一个命令的 $? 值来进行一些流程控制。
文件上传服务端 服务端的业务逻辑是这样的
1 2 3 4 5 6 7 8 9 10 11 void RecvFileMain () { while (true ){ } }
进一步的来讲是这样的,注意心跳报文,则是,如果此时接受的是<a…..则处理,最开始还没明白这是怎么处理的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 void RecvFileMain () { while (true ){ memset (strrecvbuffer, 0 , sizeof (strrecvbuffer)); memset (strsendbuffer, 0 , sizeof (strsendbuffer)); if (TcpServer.Read (strrecvbuffer, starg.timetvl + 10 ) == false ){ logfile.Write ("TcpServer.Read() failed.\n" ); return ; } logfile.Write ("strrecvbuffer = %s\n" , strrecvbuffer); if (strcmp (strrecvbuffer, "<activetest>ok</activetest>" ) == 0 ){ strcpy (strsendbuffer, "ok" ); logfile.Write ("strsendbuffer = %s\n" , strsendbuffer); if (TcpServer.Write (strsendbuffer) == false ){ logfile.Write ("TcpServer.Write() failed.\n" ); return ; } } if (strncmp (strrecvbuffer, "<filename>" , 10 ) == 0 ){ } } }
文件上传客户端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 bool _tcpputfiles(){ while (true ){ } return true ; }
上传文件内容 在建立过程中,最困难的工作就是实现传输,因此,我们有必要专门来讲解一下,如何在客户端实现发送和服务端实现接受
客户端 主框架 1 2 3 4 5 6 7 8 logfile.Write ("send %s(%d) ..." , Dir.m_FullFileName, Dir.m_FileSize); if (SendFile (TcpClient.m_connfd, Dir.m_FullFileName, Dir.m_FileSize) == true ){ logfile.Write ("ok.\n" ); }else { logfile.Write ("failed.\n" ); TcpClient.Close (); return false ; }
sendfile() 主要就是打开本地文件,然后发送本地文件,再关闭,就是有个细节,万一文件太大,无法一次打开,因此我们模拟缓冲区的思想,每次最多读1000个字节,但是注意 :又是一场毒打,找了两个晚上的错误,真的找吐了,真不应该在脑袋昏昏沉沉的时候写代码,totalbytes局部变量最开始居然没初始化,最后我检查啊检查,已经检查到了,看到surfdata2里面生成了临时文件,但是0字节,又来比对了一次,结果还是没找出来,最后通过控制变量法,用吴哥的服务端,和客户端,以此与自己的匹配,最终确定是客户端的错误,在逐个比对,最终找到,呜呜呜呜呜呜
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 bool SendFile (const int sockfd, const char *filename, const int filesize) { int onread = 0 ; int bytes = 0 ; char buffer[1000 ]; int totalbytes = 0 ; FILE *fp = NULL ; if ( (fp = fopen (filename, "rb" )) == NULL ) return false ; while (true ){ memset (buffer, 0 , sizeof (buffer)); if (filesize - totalbytes > 1000 ) onread = 1000 ; else onread = filesize - totalbytes; bytes = fread (buffer, 1 , onread, fp); if (bytes > 0 ){ if (Writen (sockfd, buffer, bytes) == false ){ fclose (fp); return false ; } } totalbytes = totalbytes + bytes; if (totalbytes == filesize) break ; } fclose (fp); return true ; }
服务端 RecvFile() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 bool RecvFile (const int sockfd, const char *filename, const char *mtime, int filesize) { while (true ) { } return true ; }
删除与转存文件 较为简单,简单看一下便是,仅仅只需要改动客户端,因为是上传功能,所以服务端没变化,客户端来决定把本地文件删除或者转存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 bool AckMessage (const char *strrecvbuffer) { char filename[301 ]; char result[11 ]; memset (filename, 0 , sizeof (filename)); memset (result, 0 , sizeof (result)); GetXMLBuffer (strrecvbuffer, "filename" , filename, 300 ); GetXMLBuffer (strrecvbuffer, "result" , result, 10 ); if (strcmp (result, "ok" ) != 0 ) return true ; if (starg.ptype == 1 ){ if (REMOVE (filename) == false ) { logfile.Write ("REMOCE(%s) failed.\n" , filename); return false ; } if (starg.ptype == 2 ){ char bakfilename[301 ]; STRCPY (bakfilename, sizeof (bakfilename), filename); UpdateStr (bakfilename, starg.clientpath, starg.clientpathbak, false ); if (RENAME (filename, bakfilename) == false ){ logfile.Write ("RENAME(%s, %s) failed.\n" , filename, bakfilename); return false ; } } } return true ; }
同步–异步通信 不管是FTP协议,还是TCP协议,都是同步通信,接下来会介绍几种异步通信的方式
同步 同步通讯效率比较低,因为把大量的时间都浪费在等待上了,但是也有个好处,就是流程很简单,一问一答这种方式,程序写起来也非常简单
异步 客户端发送n个请求,同时也会接收到很多回应,采用异步通信,不会把时间浪费在等待上面,所以效率非常高,但是也带来了新的问题,就是流程控制很麻烦,程序也会更复杂
比如说,客户端向服务端发送了1000个请求,这个时候网络断开了,此时客户端只收到了500个回应,还有500个回应没收到,此时客户端并不知道服务端到底处理成功与否,这种事情没有固定的解决方法,不同的业务有不同的方法 ,总的来说,要解决都是很麻烦的,不会太简单。
异步通信的实现
多进程,即fork(),使得父进程和子进程能够自己做自己的事情
多线程,暂时还未涉及,后面会讲
IO复用有点难,不是几句话能讲的,后面再讲
多进程实现 此时还是同步的,按部就班的,发一个接受一个
此时进行一次fork分叉,分成一个子进程和一个父进程,父进程负责发送请求,子进程负责接受回应,也就是进行了一个分工,其中,我认为由父进程来发送是很有好处的,理由是,如果用子进程来发送,pid<0为异常情况,可能就会发生,程序已经出现异常了,但仍然发送报文的情况,因此,用父进程来把控发送全局,是很好的。
这就是异步的显示情况
我们通过改大数据,发现同步 通信,每秒传输大概在2000 个报文左右,异步 通信则是80000 个,这明显不是在一个数量级的
IO复用演示
没有数据,直接返回,不会等待,有数据继续下面的流程,读取数据(现在只用知道这样就可以了,以后会详细讲解
用while循环装起来,tcpread参数改为-1
15秒开始
51秒结束
但是仔细看,我们这个程序,其实是存在缺陷的,虽然都发过去了,但是采用的IO复用,不等待,所以,服务端还没来得及回应完全部
所以,我们应该在for循环外面,再补接一下最后的结束,我们用一个计数器,来记录总共接收到的个数,确保接收完毕后及时退出for循环外的while循环(判断jj < 1000000)就退了
一百万之后,只接受ok,没有报文可以发了。
采用IO复用技术实现异步通信,代码的开销比多进程和多线程要多一些,原因就是因为while循环哪些代码的开销 ,肯定比不上让一个进程或者线程在哪里等待
文件上传(异步) 我们之前实现的上传功能是用的同步方式,接下来,我们尝试将它改成异步
目前为止,我们的程序的效率,大概每秒上传100个左右的文件
我们现在考虑采用IO技术来实现异步通信,虽然多进程和多线程的效率更高,但是进程或者线程之间,需要做同步,比较麻烦(考虑这次执行那个进程,这次执行那个线程,需要调度)
在文件上传的主函数,定义一个文件数量的变量,每收到一个 报文,这个delayed的值减一
再while循环里面,当delayed还有剩余时,疯狂TcpRead,然后delayed一没有,就退出,最后再在while循环外面加追加接受对端确认报文的函数(基本一样=-=)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 while (delayed > 0 ){ memset (strrecvbuffer, 0 , sizeof (strrecvbuffer)); if (TcpRead (TcpClient.m_connfd, strrecvbuffer, &buflen, -1 ) == false ) break ; delayed--; AckMessage (strrecvbuffer); } } while (delayed > 0 ){ memset (strrecvbuffer, 0 , sizeof (strrecvbuffer)); if (TcpRead (TcpClient.m_connfd, strrecvbuffer, &buflen, 10 ) == false ) break ; delayed--; AckMessage (strrecvbuffer); }
运行效果
5000个文件,大概在10秒左右,每秒500个左右,之前同步通信在100左右,异步比同步快五倍
收尾+优化 核心功能已经全部实现,接下来是优化细节
时间间隔优化
每上传一次,就sleep这么多秒,我们说这样不是最合理的,为什么这么说?
如果一个目录下不断地有文件生成,生成的个数和时间都是不定的,例如有20个文件,文件上传一次把20个文件给传出去了,但是传输的过程中又有文件不断生成,这些只能等到下一次sleep结束才能执行,我们可以做一些优化,每次执行文件传输任务的时候,如果有传输成功的文件,说明系统比较忙,那么在执行完这次文件传输之后,就不要sleep了,继续去执行文件传输的任务,如果某次执行了_tcpputfiles之后,文件传输返回失败,说明此时没有这么忙了,才让他去sleep。这么做应该更合理
定义一个全局的bool变量,默认为true,再判断好空闲时候的处理,进入_tcpputfiles(),进去后把bcontinue设置为false,每获得一个文件就把bcontinue设置为true就好啦
心跳优化 启用心跳
接着就是不断的考虑,在可能需要花费时间的地方做进程的心跳,更新心跳(服务端和客户端一起)
也要在服务端的每一个子进程里面做心跳,最开始使用AddPInfo 创建心跳信息,后面while里面不断更新它!
文件下载 对客户端而言,文件下载,就是服务端的文件上传主函数,文件上传客户端的代码就是服务端的文件下载主函数代码
功能完善后,将此功能加入start.sh和killall.sh脚本,并且将两个文件系统的功能衔接起来使用
1 2 3 4 5 6 7 8 9 10 11 /project/tools1/bin/procctl 10 /project/tools1/bin/fileserver 5005 /log /idc/fileserver.log /project/tools1/bin/procctl 20 /project/tools1/bin/tcpputfiles /log /idc/tcpputfiles_surfdata.log "<ip>127.0.0.1</ip><port>5005</port><ptype>1</ptype><clientpath>/tmp/ftpputest</clientpath><andchild>true</andchild><matchname>*.XML,*.CSV,*.JSON</matchname><srvpath>/tmp/tcpputest</srvpath><timetvl>10</timetvl><timeout>50</timeout><pname>tcpputfiles_surfdata</pname>" /project/tools1/bin/procctl 20 /project/tools1/bin/tcpgetfiles /log /idc/tcpgetfiles_surfdata.log "<ip>127.0.0.1</ip><port>5005</port><ptype>1</ptype><srvpath>/tmp/tcpputest</srvpath><andchild>true</andchild><matchname>*.XML,*.CSV,*.JSON</matchname><clientpath>/tmp/tcpgetest</clientpath><timetvl>10</timetvl><timeout>50</timeout><pname>tcpgetfiles_surfdata</pname>" /project/tools1/bin/procctl 300 /project/tools1/bin/deletefiles /tmp/tcpgetest "*" 0.02
学习总结
计算机网络基础,一定要系统的学习,面试的常考点,并且理论和实践结合才能走的更远
没有封装的socketAPI,我们不知道该怎么开始实现程序
多进程是一种比较传统的方法,对高并发系统不适用,但是对并发需求不是这么高的,是一种不错的选择,程序流程简单,代码实现也比较容易。
实现采用TCP协议的文件传输和下载,这个是十分重要的系统,以后我们做的很多项目都可以使用它,只要学好了网络编程的基础知识,实现文件传输没有什么问题,重要的是采用异步通信提升性能,但是异步通信的实现就会带来编写代码的困难
例如每个月有很多企业向银行发送转账信息,但是银行首先得保证自己不死,然后对每个企业进行限流,如果企业发多了就返回转账失败,这里我们做流量控制,方法有很多,最常用的就是滑动窗口,也是企业常用的,面试常考的
滑动窗口 计网学完记得闪耀回归~
五、MySQL数据库开发
暂时不研究connection和sqlstatement的底层封装,现在能力有限
我们现在开始走马观花的跑一遍头文件
filetobuf和buftofile用于操纵二进制文件
CDA_DEF用于存放对数据操作的结果
connection连接类,有几个主要的功能,连接,提交,回滚,断开
sqlstatement类用于操纵数据
这些都是到时候再说,下面我们来完成建库操作
MySQL建库 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include "_mysql.h" int main (int argc, char *argv[]) { connection conn; if (conn.connecttodb ("127.0.0.1,root,123456,h2,3306" ,"utf8" ) != 0 ){ printf ("connect database failed.\n%s\n" , conn.m_cda.message); return -1 ; } sqlstatement stmt (&conn) ; stmt.prepare ("create table girls(id bigint(10),\ name varchar(30),\ weight decimal(8,2),\ btime datetime,\ memo longtext,\ pic longblob,\ primary key (id))" ); if (stmt.execute ()!=0 ){ printf ("stmt.execute() failed.\n%s\n%s\n" ,stmt.m_sql,stmt.m_cda.message); return -1 ; } return 0 ; }
插入数据 插入,修改代码流程基本完全一样
插入数据的程序虽然没能跑出来,但业务逻辑大抵是这样的:
登录数据库
定义存放单个信息的结构体
将结构体与操作数据库对象的每一个参数连接起来
执行插入
清空结构体一次
注入信息一次
执行一次操作:execute()
提交事务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #include "_mysql.h" int main (int argc, char *argv[]) { connection conn; if (conn.connecttodb ("127.0.0.1,root,123456,h2,3306" ,"utf8" ) != 0 ){ printf ("connect database failed.\n%s\n" , conn.m_cda.message); return -1 ; } struct st_girls { long id; char name[31 ]; double weight; char btime[20 ]; } stgirls; sqlstatement stmt (&conn) ; stmt.prepare ("\ insert into girls(id,name,weight,btime) values(:1,:2,:3,str_to_date(:4,'%%Y-%%m-%%d %%H:%%i:%%s'))" ); stmt.bindin (1 , &stgirls.id); stmt.bindin (2 , stgirls.name, 30 ); stmt.bindin (3 , &stgirls.weight); stmt.bindin (4 , stgirls.btime, 19 ); for (int i = 0 ; i < 5 ; i++){ memset (&stgirls, 0 , sizeof (struct st_girls)); stgirls.id = i + 1 ; sprintf (stgirls.name, "西施%05dgirl" , i + 1 ); stgirls.weight = 45.25 + i; sprintf (stgirls.btime, "2022-05-07 14:47:%02d" , i); if (stmt.execute () != 0 ){ printf ("stmt.execute() failed.\n%s\n%s\n" , stmt.m_sql, stmt.m_cda.message); return -1 ; } printf ("成功的插入了1条记录" ); } printf ("insert table girls ok.\n" ); conn.commit (); return 0 ; }
查询数据 手动执行查询语句
与其他语句最大的不同点,在于查询语句需要返回一个结果集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 int iminid,imaxid; stmt.prepare ("\ select id,name,weight,date_format(btime,'%%Y-%%m-%%d %%H:%%i:%%s') from girls where id>=:1 and id<=:2" ); stmt.bindin (1 ,&iminid); stmt.bindin (2 ,&imaxid); stmt.bindout (1 ,&stgirls.id); stmt.bindout (2 , stgirls.name,30 ); stmt.bindout (3 ,&stgirls.weight); stmt.bindout (4 , stgirls.btime,19 ); iminid=1 ; imaxid=3 ; if (stmt.execute () != 0 ) { printf ("stmt.execute() failed.\n%s\n%s\n" ,stmt.m_sql,stmt.m_cda.message); return -1 ; } while (true ) { memset (&stgirls,0 ,sizeof (struct st_girls)); if (stmt.next ()!=0 ) break ; printf ("id=%ld,name=%s,weight=%.02f,btime=%s\n" ,stgirls.id,stgirls.name,stgirls.weight,stgirls.btime); } printf ("本次查询了girls表%ld条记录。\n" ,stmt.m_cda.rpc); return 0 ; }
二进制大对象 将本地大对象存入数据库里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 struct st_girls { long id; char pic[100000 ]; unsigned long picsize; } stgirls; sqlstatement stmt (&conn) ; stmt.prepare ("update girls set pic=:1 where id=:2" ); stmt.bindinlob (1 , stgirls.pic,&stgirls.picsize); stmt.bindin (2 ,&stgirls.id); for (int ii=1 ;ii<3 ;ii++) { memset (&stgirls,0 ,sizeof (struct st_girls)); stgirls.id=ii; if (ii==1 ) stgirls.picsize=filetobuf ("1.jpg" ,stgirls.pic); if (ii==2 ) stgirls.picsize=filetobuf ("2.jpg" ,stgirls.pic); if (stmt.execute ()!=0 ) { printf ("stmt.execute() failed.\n%s\n%s\n" ,stmt.m_sql,stmt.m_cda.message); return -1 ; } printf ("成功修改了%ld条记录。\n" ,stmt.m_cda.rpc); } printf ("update table girls ok.\n" ); conn.commit (); return 0 ; }
从数据库取出大对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 stmt.prepare ("select id,pic from girls where id in (1,2)" ); stmt.bindout (1 ,&stgirls.id); stmt.bindoutlob (2 , stgirls.pic,100000 ,&stgirls.picsize); if (stmt.execute ()!=0 ) { printf ("stmt.execute() failed.\n%s\n%s\n" ,stmt.m_sql,stmt.m_cda.message); return -1 ; } while (true ) { memset (&stgirls,0 ,sizeof (stgirls)); if (stmt.next ()!=0 ) break ; char filename[101 ]; memset (filename,0 ,sizeof (filename)); sprintf (filename,"%d_out.jpg" ,stgirls.id); buftofile (filename,stgirls.pic,stgirls.picsize); } printf ("本次查询了girls表%ld条记录。\n" ,stmt.m_cda.rpc); return 0 ; }
大对象操作的启示 这样数据库就不会有压力~~
数据库开发注意事项和技巧 注意事项 一个connection对象同一时间仅连一个数据库,多个connection可以同时连多个数据库,并且每个对象之间的处理,不互相影响,按照顺序依次执行
这个很容易理解,试想一下,如果一个进程要求提交事务,另一个进程却想要回滚,自然只会满足一个的需求,也就是报错了,虽然我们可以选择,比如给数据库连接加锁之类的方法,但是,他仍然是很麻烦的,不如我们先fork()后再登陆
这样的话,就没有问题了
这句话也就是说,在一个数据库的连接中,可以执行多条sql语句(sqlstatement是执行sql语句的对象)
并且注意,在这里直接execute(),并没有使用prepare,是因为我们封装了方法,如果没有绑定输入和输出变量,那我们可以直接execute(),这样是为了我们写代码的方便
这一点是MySQL的缺点,给我们带来了不少麻烦
2014错误,百度的大概意思就是上面的,没有取完之前不能执行,增加一行代码while(只取结果集),最后就不会报错了,具体解决方法,等以后有应用背景了再回来激战
应用技巧
首先我们来演示在命令行是如何处理的….
1 2 3 4 5 6 7 8 9 10 insert into girls(id) values(30); insert into girls(id, weight) values(31, null); insert into girls(id, weight) values(32, '45.33'); insert into girls(id, weight) values(33, ''); -- ERROR 1366 (HY000): Incorrect decimal value: '' for column 'weight' at row 1 select * from girls; | 30 | NULL | NULL | NULL | NULL | NULL | | 31 | NULL | NULL | NULL | NULL | NULL | | 32 | NULL | 45.33 | NULL | NULL | NULL | +----+-----------------+--------+---------------------+------+------+
其次,在程序中
1 stmt.prepare ("insert into girls(id,name,weight,btime) values(:1,:2,:3,str_to_date(:4,'%%Y-%%m-%%d %%H:%%i:%%s'))" );
我们不可能提前知道weight会不会填 空或者不写
有一个解决方法,我们把体重改成字符串
1 double weight; // 超女体重 ----------------> char weight[10];
用框架bindin,可以直接填''
,因为我们做了封装,现在不研究,因为并不容易,以后有实力的再研究
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 for (int ii=0 ;ii<maxbindin;ii++) { if (params_in[ii].buffer_type == MYSQL_TYPE_VAR_STRING ) { if (strlen ((char *)params_in[ii].buffer)==0 ) { params_in_is_null[ii]=true ; } else { params_in_is_null[ii]=false ; params_in_length[ii]=strlen ((char *)params_in[ii].buffer); } } if (params_in[ii].buffer_type == MYSQL_TYPE_BLOB ) { if ((*params_in[ii].length)==0 ) params_in_is_null[ii]=true ; else params_in_is_null[ii]=false ; } }
注意 :这个技巧非常重要,以后工作中应该用得上,面试也可能会被问起
我们可以理解为:只要不做计算 ,我们对数据的存放,都统一采用字符串!
当然,数据变为字符串,对于浮点数 而言我们通常需要将其变为整数,方便存储,这里定义一个临时变量(和wf一样,char类型 [11])
1 2 snprintf (stzhobtmind.wf,10 ,"%d" ,(int )(atof (tmp)*10 ));
强大的PowerDesigner 自增字段,一定要设置为键,而不是组件
最重要的,表名(General),列(Columns),索引(Indexes),键(Keys)
次重要 物理选项(Physical Options):可能以后会设置一些物理参数
站点参数文件入库 这里我们继续做项目……
这里有一个细节,为什么经纬度明明是浮点数,我们却用整数,原因很简单:
整数运算速度快,相比于浮点数复杂的加减乘除
整数可以代替浮点数,只要我们记住了保留小数点后多少位
例如在银行查询余额,如果还剩下1.25元,那么可以使用125在底层存放,只需要在对应的位置加上小数点就可以了
这样我们使用的存储空间更小 ,运算的效率也更高
业务逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 int main (int argc, char *argv[]) { for (int i = 0 ; i < vstcode.size (); i++){ } return 0 ; }
这里我们提几点
为什么要用vstcode加载到容器中,而不直接打开文件,一行一行的循环读入呢?
处理方法本来就不唯一,习惯哪一种写法,就写哪一种写法
连接数据库的代码一定要放在加载参数文件之后,如果加载参数失败,后面的流程根本就不需要继续,数据库也不用连了。
判断记录已存在的方法,是判断SQL语句返回的结果,如果记录已存在,那么插入记录返回的结果是1062
开始战斗 在我们的表里,将mysql数据库的height故意设置为可以为空,这里是为了处理之前留下的应用技巧(就上上上个标题)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 mysql> desc T_ZHOBTCODE; +----------+-------------+------+-----+-------------------+-----------------------------+ | Field | Type | Null | Key | Default | Extra | +----------+-------------+------+-----+-------------------+-----------------------------+ | obtid | varchar(10) | NO | PRI | NULL | | | cityname | varchar(30) | NO | | NULL | | | provname | varchar(30) | NO | | NULL | | | lat | int(11) | NO | | NULL | | | lon | int(11) | NO | | NULL | | | height | int(11) | YES | | NULL | | | upttime | timestamp | NO | | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP | | keyid | int(11) | NO | MUL | NULL | | +----------+-------------+------+-----+-------------------+-----------------------------+ 8 rows in set (0.00 sec)
1 2 3 4 5 6 7 8 9 10 struct st_stcode { char provname[31 ]; char obtid[11 ]; char cityname[31 ]; char lat[11 ]; char lon[11 ]; char height[11 ]; };
心跳进程小技巧 由于该程序通常来讲执行之间都不会超过1s,所以心跳处理非常简单,并且如果要用GDB调试,可以考虑注释掉心跳(防止被守护进程杀掉),或者心跳时间调的足够长
1 2 3 PActive.AddPInfo (10 ,"obtcodetodb" );
站点数据入库
基础业务逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 bool _obtmindtodb(char *pathname,char *connstr,char *charset){ while (true ){ while (true ){ } } return true ; }
接下来我们要创建表
其中有一个细节:
对这个唯一索引而言,是时间在前,obtid在后
而对于主键索引而言,是先obtid(站点代码)后数据时间
因此,写这两行语句,利用的东西是不同的。
对于这个程序,我们只提供插入功能,不提供修改功能,原因是这样的:
观测数据是由观测设备产生,比如说温度,设备感应到是35度,就是35度,这个错了就错了,没办法改变,如果设备的传感器出了问题,可以把数据标志为不可用,但是修改数据是不可能的,这么多设备,产生那么多数据,根本改不过来的。
也可以换一种思维方式:设备已经出问题了,那么就没有正确的数据了,那么,既然没有正确的数据,哪来的修改?
正式业务demo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 bool _obtmindtodb(char *pathname,char *connstr,char *charset){ sqlstatement stmt; CDir Dir; if (Dir.OpenDir (pathname, "*.xml" ) == false ){ logfile.Write ("Dir.OpenDir(%s) failed.\n" , pathname); return false ; } CFile File; while (true ){ if (Dir.ReadDir () == false ) break ; if (conn.m_state == 0 ){ if (conn.connecttodb (connstr, charset)!=0 ) { logfile.Write ("connect database(%s) failed.\n%s\n" , connstr, conn.m_cda.message); return -1 ; } } logfile.Write ("connect database(%s) ok.\n" , connstr); if (stmt.m_state == 0 ){ stmt.connect (&conn); stmt.prepare ("insert into T_ZHOBTMIND(obtid, ddatetime, t, p, u, wd, wf, r, vis) \ values(:1, str_to_date(:2, '%%Y%%m%%d%%H%%i%%s'), :3, :4, :5, :6, :7, :8, :9)" ); stmt.bindin (1 , stzhobtmind.obtid, 10 ); } logfile.Write ("filename = %s\n" , Dir.m_FullFileName); if (File.Open (Dir.m_FullFileName, "r" ) == false ){ logfile.Write ("File.Open(%s) failed.\n" , Dir.m_FullFileName); return false ; } char strBuffer[1001 ]; while (true ){ if (File.FFGETS (strBuffer, 1000 , "<endl/>" ) == false ) break ; logfile.Write ("strBuffer=%s" , strBuffer); memset (&stzhobtmind, 0 , sizeof (struct st_zhobtmind)); GetXMLBuffer (strBuffer, "obtid" , stzhobtmind.obtid, 10 ); GetXMLBuffer (strBuffer,"ddatetime" ,stzhobtmind.ddatetime,14 ); char tmp[11 ]; GetXMLBuffer (strBuffer, "t" , tmp, 10 ); if (strlen (tmp) > 0 ) snprintf (stzhobtmind.t, 10 , "%d" , (int )(atof (tmp)*10 )); GetXMLBuffer (strBuffer,"u" ,stzhobtmind.u,10 ); if (stmt.execute () != 0 ){ if (stmt.m_cda.rc != 1062 ){ logfile.Write ("Buffer = %s\n" , strBuffer); logfile.Write ("stmt.execute() failed.\n%s\n%s\n" , stmt.m_sql, stmt.m_cda.message); } } } conn.commit (); } return true ; }
优化业务 任务一:优化日志内容 现在的太乱,基本没法看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 bool _obtmindtodb(char *pathname,char *connstr,char *charset){ int totalcount = 0 ; int insertcount = 0 ; CTimer Timer; while (true ){ totalcount = insertcount = 0 ; while (true ){ totalcount++; } if (stmt.execute () != 0 ){ ........ }else insertcount++; } logfile.Write ("已处理文件%s(totalcount=%d, insertcount = %d), 耗时%.2f秒。\n" , \ Dir.m_FullFileName, totalcount, insertcount, Timer.Elapsed ()); return true ; }
现在的日志就非常的整齐,不过我们可以看到还有一个问题,就是totalcount并不是单个文件的totalcount,所以我们可以在打开文件的上端初始化totalcount 和 insertcount,这里insertcount为何为0,其实是因为,之前已经插入过一次了,但是数据来说,还是旧数据反复运行,所以自然有了的就不会再插入咯
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 TYPE="Ethernet" PROXY_METHOD="none" BROWSER_ONLY="no" BOOTPROTO=static DEFROUTE="yes" IPV4_FAILURE_FATAL="no" IPV6INIT="yes" IPV6_AUTOCONF="yes" IPV6_DEFROUTE="yes" IPV6_FAILURE_FATAL="no" IPV6_ADDR_GEN_MODE="stable-privacy" NAME="ens33" UUID="68f5143d-d91c-4c4c-b55e-523ee2c7ee02" DEVICE="ens33" ONBOOT="yes" IPADDR=192.168.211.129 NETMASK=255.255.255.0 GATEWAY=192.168.133.1 DNS1=114.114.114.114
任务二:处理冗余的程序 我们可以想象,如果我们的操作越来越多,那么程序只会越来越复杂,比如这里提到的表的字段50个,那我们光绑定参数,都要花100行,显然这是很没有必要的空间花费,我们说,还是想让程序变得优雅起来的。
接下来我们尝试把数据结构,数据解析,和对表的操作封装成一个类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 class CZHOBTMIND { public : connection *m_conn; CLogFile *m_logfile; sqlstatement m_stmt; char m_buffer[1024 ]; struct st_zhobtmind m_zhobtmind ; CZHOBTMIND (); CZHOBTMIND (connection *conn, CLogFile *logfile); ~CZHOBTMIND (); void BindConnLog (connection *conn, CLogFile *logfile) ; bool SplitToBuffer (char *strBuffer) ; bool InsertTable () ; }; CZHOBTMIND::CZHOBTMIND (){ m_conn=0 ; m_logfile=0 ; } CZHOBTMIND::CZHOBTMIND (connection *conn, CLogFile *logfile){ m_conn=conn; m_logfile=logfile; } CZHOBTMIND::~CZHOBTMIND (){ } void CZHOBTMIND::BindConnLog (connection *conn, CLogFile *logfile) { m_conn=conn; m_logfile=logfile; } bool CZHOBTMIND::SplitToBuffer (char *strBuffer) { memset (&m_zhobtmind, 0 , sizeof (struct st_zhobtmind)); GetXMLBuffer (strBuffer, "obtid" , m_zhobtmind.obtid, 10 ); char tmp[11 ]; GetXMLBuffer (strBuffer, "t" , tmp, 10 ); if (strlen (tmp) > 0 ) snprintf (m_zhobtmind.t, 10 , "%d" , (int )(atof (tmp)*10 )); ..... STRCPY (m_buffer, sizeof (m_buffer), strBuffer); return true ; } bool CZHOBTMIND::InsertTable () { if (m_stmt.m_state == 0 ){ m_stmt.connect (m_conn); m_stmt.prepare ("insert into T_ZHOBTMIND(obtid, ddatetime, t, p, u, wd, wf, r, vis) \ values(:1, str_to_date(:2, '%%Y%%m%%d%%H%%i%%s'), :3, :4, :5, :6, :7, :8, :9)" ); m_stmt.bindin (1 , m_zhobtmind.obtid, 10 ); ...... } if (m_stmt.execute () != 0 ){ if (m_stmt.m_cda.rc != 1062 ){ m_logfile -> Write ("Buffer = %s\n" , m_buffer); m_logfile -> Write ("m_stmt.execute() failed.\n%s\n%s\n" , m_stmt.m_sql, m_stmt.m_cda.message); } return false ; } return true ; } bool _obtmindtodb(char *pathname,char *connstr,char *charset){ totalcount++; ZHOBTMIND.SplitToBuffer (strBuffer); if (ZHOBTMIND.InsertTable () == true ) insertcount++; }
这样的话,无论代码有多长(插入绑定bindin,解析xml),我们都只需要两行 。
任务三:程序模块分离 我们仔细思考不难得到,在数据中心项目中,这种得到数据,然后上传库的程序,他们的逻辑是完全相同的,并且可以这么说,有多少种数据,就得到了多少个程序。并且,我们定义的结构体,在其他程序中也有可能用得上(存放站点代码,数据时间,温度….),我们定义的数据操作类也是同样的道理,那么,这些代码是不是可以分离出去?
我们可以这样,为这个项目,创造一个头文件和一个cpp文件。
结构体和类的声明分离到头文件中
类的实现代码,分离到cpp文件中
所以到了这里,我又去百度了头文件,cpp文件这些标准写法,果然不断学习,就会不断有新的感悟.
(23条消息) C语言笔记——自定义头文件_Sunrise的博客的博客-CSDN博客_c语言自定义头文件
定义了idcapp.h 和 idcapp.cpp将其冗余部分存放
任务四:对csv格式的支持 进入业务处理主函数
修改Dir.OpenDir里面的代码,加入支持读入*.csv
加入bool变量isxml true 为 xml false 为 csv
读取目录,得到数据文件名后,判断他的后缀,根据后缀修改isxml的值
读取文件的每一行时候,更改读取结束时候的判断条件
SplitBuffer增加传入参数 bisxml
一个细节报错 这里面这个if语句,是有歧义的
1 2 3 4 5 6 7 8 9 10 char strBuffer[1001 ]; while (true ){ if (isxml == true ) if (File.FFGETS (strBuffer, 1000 , "<endl/>" ) == false ) break ; else { if (File.Fgets (strBuffer, 1000 , true ) == false ) break ; if (strstr (strBuffer, "站点" ) != 0 ) continue ; } totalcount++;
他还可以解析成这样
1 2 3 4 5 6 if (isxml == true ) if (File.FFGETS (strBuffer, 1000 , "<endl/>" ) == false ) break ; else { if (File.Fgets (strBuffer, 1000 , true ) == false ) break ; if (strstr (strBuffer, "站点" ) != 0 ) continue ; }
另外一个惨痛教训=-=。由于OpenDir不是我封装的,我习惯性的打了空格
1 2 3 4 5 6 CDir Dir; if (Dir.OpenDir (pathname, "*.xml, *.csv" ) == false ){ logfile.Write ("Dir.OpenDir(%s) failed.\n" , pathname); return false ; }
最后,就csv测试,日志没有任何东西(我把xml的文件都删除了,只留下csv的),奇了怪了=-=,原来不能加空格啊!
1 2 3 4 5 6 CDir Dir; if (Dir.OpenDir (pathname, "*.xml,*.csv" ) == false ){ logfile.Write ("Dir.OpenDir(%s) failed.\n" , pathname); return false ; }
最后顺利运行。
最后收尾工作 启用删除业务代码
心跳从5000s改为10s
1 2 3 PActive.AddPInfo (30 ,"obtmindtodb" );
现在,我们已经彻底完工,不过,还不能让程序通过调度程序跑起来,因为一旦跑起来之后,数据是无穷无尽的,很快就会把磁盘空间给占满,因此,我们还可以提供一个小的脚本文件,来使得动态的删除清空
执行SQL脚本文件 现在我们有一个新的需求,希望定期执行sql脚本,并清理历史数据,就像之前开发的,清理历史文件一样
在linux下面,可以直接这样执行,注意只是报警,并没有报错,不过这里使用了输入重定向功能,而我们的调度程序并不支持,所以只能自己写hhhc
流程 程序流程也十分简单,打开文件,一行一行的读取出来,执行,参数也无需绑定
具体过程是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 if (logfile.Open (argv[4 ], "a+" ) == false ){ printf ("打开日志文件失败(%s).\n" , argv[4 ]); return -1 ; } PActive.AddPInfo (500 ,"obtcodetodb" ); if (conn.connecttodb (argv[2 ], argv[3 ], 1 )!=0 ){ logfile.Write ("connect database(%s) failed.\n%s\n" , argv[2 ], conn.m_cda.message); return -1 ; } logfile.Write ("connect database(%s) ok.\n" , argv[2 ]); CFile File; if (File.Open (argv[1 ], "r" )==false ){ logfile.Write ("File.Open(%s) failed.\n" , argv[1 ]); EXIT (-1 ); } char strsql[1001 ]; while (true ) { memset (strsql, 0 , sizeof (strsql)); if (File.FFGETS (strsql, 1000 , ";" ) == false ) break ; if (strsql[0 ] == '#' ) continue ; char *pp = strstr (strsql, ";" ); if (pp == 0 ) continue ; pp[0 ] = 0 ; logfile.Write ("%s\n" , strsql); int iret = conn.execute (strsql); if (iret == 0 ) logfile.Write ("exec ok(rpc=%d).\n" , conn.m_cda.rpc); else logfile.Write ("exec failed(%s).\n" , conn.m_cda.message); PActive.UptATime (); } logfile.WriteEx ("\n" );
接下来再做之前,做镜像,把网络连上,首先换源阿里云,然后配置ftp环境,重新启动调度一系列
搭建ftp
CentOS搭建ftp服务器 - ismallboy - 博客园 (cnblogs.com)
Centos之FTP服务器的搭建与配置_Qwzf的博客-CSDN博客_centos 连接ftp服务器
换源
CentOS7更换国内源 - Jankin-Wen - 博客园 (cnblogs.com)
CentOS7更换国内源 - 蝉声且送阳西 - 博客园 (cnblogs.com)
固定ip
centos配置固定IP(只需三步)_周星星_9527的博客-CSDN博客_centos 固定ip
虚拟机VMware下载与安装教程(详细)_-借我杀死庸碌的情怀-的博客-CSDN博客_vmware
(23条消息) 解决CentOS7虚拟机无法上网并设置CentOS7虚拟机使用静态IP上网_a785975139的博客-CSDN博客_centos7虚拟机网络配置
Linux Centos 安装配置,Centos7设置静态IP地址不能上网 - 蕃薯耀 - 博客园 (cnblogs.com)
六、开发数据抽取子系统
对于每个子系统,它们都会拥有它们自己的数据库,那么我们需要思考一下,如何将每一个子系统的数据,融入数据中心总系统中。
有一种简单的想法,是可以直接在两个系统之间建立connect类,然后通过插入语句使得两表相连,这样是一个思路,但也有一些问题,其中主要的就是有的时候我们不能直接操纵两个表。
比如政府,省气象局等等的数据,我们不可能直接调用它的数据库,因此采用一个数据抽取的模块,利用模块化编程的思想,会使得我们的工作更佳轻松,并且条例清晰
开发目标
搭建框架 宏结构体 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 struct st_arg { char connstr[101 ]; char charset[51 ]; char selectsql[1024 ]; char fieldstr[501 ]; char fieldlen[501 ]; char bfilename[31 ]; char efilename[31 ]; char outpath[301 ]; char starttime[52 ]; char incfield[31 ]; char incfilename[301 ]; int timeout; char pname[51 ]; } starg; #define MAXFIELDCOUNT 100 int MAXFIELDLEN=-1 ; char strfieldname[MAXFIELDCOUNT][31 ]; int ifieldlen[MAXFIELDCOUNT]; int ifieldcount; int incfieldpos=-1 ;
帮助文档 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 void _help(){ printf ("Using:/project/tools1/bin/dminingmysql logfilename xmlbuffer\n\n" ); printf ("Sample:/project/tools1/bin/procctl 3600 /project/tools1/bin/dminingmysql /log/idc/dminingmysql_ZHOBTCODE.log \"<connstr>127.0.0.1,root,mysqlpwd,mysql,3306</connstr><charset>gbk</charset><selectsql>select obtid,cityname,provname,lat,lon,height from T_ZHOBTCODE</selectsql><fieldstr>obtid,cityname,provname,lat,lon,height</fieldstr><fieldlen>10,30,30,10,10,10</fieldlen><bfilename>ZHOBTCODE</bfilename><efilename>HYCZ</efilename><outpath>/idcdata/dmindata</outpath><timeout>30</timeout><pname>dminingmysql_ZHOBTCODE</pname>\"\n\n" ); printf (" /project/tools1/bin/procctl 30 /project/tools1/bin/dminingmysql /log/idc/dminingmysql_ZHOBTMIND.log \"<connstr>127.0.0.1,root,mysqlpwd,mysql,3306</connstr><charset>gbk</charset><selectsql>select obtid,date_format(ddatetime,'%%%%Y-%%%%m-%%%%d %%%%H:%%%%i:%%%%s'),t,p,u,wd,wf,r,vis,keyid from t_zhobtmind where keyid>:1 and ddatetime>timestampadd(minute,-120,now())</selectsql><fieldstr>obtid,ddatetime,t,p,u,wd,wf,r,vis,keyid</fieldstr><fieldlen>10,19,8,8,8,8,8,8,8,15</fieldlen><bfilename>ZHOBTMIND</bfilename><efilename>HYCZ</efilename><outpath>/idcdata/dmindata</outpath><starttime></starttime><incfield>keyid</incfield><incfilename>/idcdata/dmining/dminingmysql_ZHOBTMIND_HYCZ.list</incfilename><timeout>30</timeout><pname>dminingmysql_ZHOBTMIND_HYCZ</pname>\"\n\n" ); printf ("本程序是数据中心的公共功能模块,用于从mysql数据库源表抽取数据,生成xml文件。\n" ); printf ("logfilename 本程序运行的日志文件。\n" ); printf ("xmlbuffer 本程序运行的参数,用xml表示,具体如下:\n\n" ); printf ("connstr 数据库的连接参数,格式:ip,username,password,dbname,port。\n" ); printf ("charset 数据库的字符集,这个参数要与数据源数据库保持一致,否则会出现中文乱码的情况。\n" ); printf ("selectsql 从数据源数据库抽取数据的SQL语句,注意:时间函数的百分号%需要四个,显示出来才有两个,被prepare之后将剩一个。\n" ); printf ("fieldstr 抽取数据的SQL语句输出结果集字段名,中间用逗号分隔,将作为xml文件的字段名。\n" ); printf ("fieldlen 抽取数据的SQL语句输出结果集字段的长度,中间用逗号分隔。fieldstr与fieldlen的字段必须一一对应。\n" ); printf ("bfilename 输出xml文件的前缀。\n" ); printf ("efilename 输出xml文件的后缀。\n" ); printf ("outpath 输出xml文件存放的目录。\n" ); printf ("starttime 程序运行的时间区间,例如02,13表示:如果程序启动时,踏中02时和13时则运行,其它时间不运行。" \ "如果starttime为空,那么starttime参数将失效,只要本程序启动就会执行数据抽取,为了减少数据源" \ "的压力,从数据库抽取数据的时候,一般在对方数据库最闲的时候时进行。\n" ); printf ("incfield 递增字段名,它必须是fieldstr中的字段名,并且只能是整型,一般为自增字段。" \ "如果incfield为空,表示不采用增量抽取方案。" ); printf ("incfilename 已抽取数据的递增字段最大值存放的文件,如果该文件丢失,将重新抽取全部的数据。\n" ); printf ("timeout 本程序的超时时间,单位:秒。\n" ); printf ("pname 进程名,尽可能采用易懂的、与其它进程不同的名称,方便故障排查。\n\n\n" ); }
参数解析 这里主要体现最核心的改动,是基于宏结构体新加入的结果段而制作的,因为我们现在要做的是一个通用的模块,所以我们必须考虑兼容的情况,就会多花一些功夫
主要功能是:
获取字段数量
获取每一个字段的name
核查字段数量是否一一对应
得到想要字段的索引
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 bool _xmltoarg(char *strxmlbuffer){ memset (&starg,0 ,sizeof (struct st_arg)); .................... CCmdStr CmdStr; CmdStr.SplitToCmd (starg.fieldlen,"," ); if (CmdStr.CmdCount ()>MAXFIELDCOUNT){ logfile.Write ("fieldlen的字段数太多,超出了最大限制%d。\n" ,MAXFIELDCOUNT); return false ; } for (int ii=0 ;ii<CmdStr.CmdCount ();ii++){ CmdStr.GetValue (ii,&ifieldlen[ii]); if (ifieldlen[ii]>MAXFIELDLEN) MAXFIELDLEN=ifieldlen[ii]; } ifieldcount=CmdStr.CmdCount (); CmdStr.SplitToCmd (starg.fieldstr,"," ); if (CmdStr.CmdCount ()>MAXFIELDCOUNT){ logfile.Write ("fieldstr的字段数太多,超出了最大限制%d。\n" ,MAXFIELDCOUNT); return false ; } for (int ii=0 ;ii<CmdStr.CmdCount ();ii++){ CmdStr.GetValue (ii,strfieldname[ii],30 ); } if (ifieldcount!=CmdStr.CmdCount ()){ logfile.Write ("fieldstr和fieldlen的元素数量不一致。\n" ); return false ; } if (strlen (starg.incfield)!=0 ){ for (int ii=0 ;ii<ifieldcount;ii++) if (strcmp (starg.incfield,strfieldname[ii])==0 ) { incfieldpos=ii; break ; } if (incfieldpos==-1 ){ logfile.Write ("递增字段名%s不在列表%s中。\n" ,starg.incfield,starg.fieldstr); return false ; } } return true ; }
全量抽取数据主功能 全量抽取就是原封不动的抽取数据,比较适合全国站点参数表,因为毕竟站点可能就几千个。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 bool _dminingmysql(){ sqlstatement stmt (&conn) ; stmt.prepare (starg.selectsql); char strfieldvalue[ifieldcount][MAXFIELDLEN+1 ]; for (int ii=1 ;ii<=ifieldcount;ii++){ stmt.bindout (ii,strfieldvalue[ii-1 ],ifieldlen[ii-1 ]); } if (stmt.execute ()!=0 ){ logfile.Write ("stmt.execute() failed.\n%s\n%s\n" ,stmt.m_sql,stmt.m_cda.message); return false ; } CFile File; while (true ){ memset (strfieldvalue,0 ,sizeof (strfieldvalue)); if (stmt.next ()!=0 ) break ; if (File.IsOpened () == false ){ crtxmlfilename (); if (File.OpenForRename (strxmlfilename, "w+" ) == false ){ logfile.Write ("File.OpenForRename(%s) failed. \n" , strxmlfilename); return false ; } File.Fprintf ("<data>\n" ); } for (int ii=1 ;ii<=ifieldcount;ii++) File.Fprintf ("<%s>%s</%s>" ,strfieldname[ii-1 ],strfieldvalue[ii-1 ],strfieldname[ii-1 ]); File.Fprintf ("<endl/>\n" ); } if (File.IsOpened () == true ){ File.Fprintf ("</data>\n" ); if (File.CloseAndRename () == false ){ logfile.Write ("File.CloseAndRename(%s) failed.\n" , strxmlfilename); return false ; } logfile.Write ("生成文件%s(%d). \n" , strxmlfilename, stmt.m_cda.rpc); } return true ; } void crtxmlfilename () { char strLocalTime[21 ]; memset (strLocalTime, 0 , sizeof (strLocalTime)); LocalTime (strLocalTime, "yyyymmddhh24miss" ); SNPRINTF (strxmlfilename, 300 , sizeof (strxmlfilename), "%s/%s_%s_%s.xml" , starg.outpath, starg.bfilename, strLocalTime, starg.efilename); }
进一步优化 我们的上传文件模块虽然能成功的上传标准xml格式模块,但是在实际开发中,可能会涉及数据条数很多,从而导致,如果一次性上传,会对数据库造成一定时间内不小的冲击压力,因此我们使用分块的思想,这里设定每次只上传1000条
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 修改点1 : File.Fprintf ("<endl/>\n" ); } -------------------------------------------- File.Fprintf ("<endl/>\n" ); if (stmt.m_cda.rpc%1000 == 0 ){ File.Fprintf ("</data>\n" ); if (File.CloseAndRename () == false ){ logfile.Write ("File.CloseAndRename(%s) failed.\n" , strxmlfilename); return false ; } logfile.Write ("生成文件%s(1000). \n" , strxmlfilename); } } 修改点2 : void crtxmlfilename () { 由于命名方式有时间,并且最多只精确到s,如果同一秒内生成多个文件就会重复,所以我们新添加一个序号 --> void crtxmlfilename () { char strLocalTime[21 ]; memset (strLocalTime, 0 , sizeof (strLocalTime)); LocalTime (strLocalTime, "yyyymmddhh24miss" ); static int iseq = 1 ; SNPRINTF (strxmlfilename, 300 , sizeof (strxmlfilename), "%s/%s_%s_%s_%d.xml" , starg.outpath, starg.bfilename, strLocalTime, starg.efilename, iseq++); }
增量抽取 这里的增量抽取和百度的不太一样,但增量抽取就是为了记住位置,方便下一步的操作
业务逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 bool _dminingmysql(){ readincfile (); if (strlen (starg.incfield) != 0 ) stmt.bindin (1 , &imaxincvalue); if (stmt.execute ()!=0 ){ logfile.Write ("stmt.execute() failed.\n%s\n%s\n" ,stmt.m_sql,stmt.m_cda.message); return false ; } if ( (strlen (starg.incfield)!=0 ) && (imaxincvalue<atol (strfieldvalue[incfieldpos])) ) imaxincvalue=atol (strfieldvalue[incfieldpos]); if (stmt.m_cda.rpc>0 ) writeincfile (); } bool readincfile () { imaxincvalue=0 ; if (strlen (starg.incfield)==0 ) return true ; CFile File; if (File.Open (starg.incfilename,"r" )==false ) return true ; char strtemp[31 ]; File.FFGETS (strtemp, 30 ); imaxincvalue = atol (strtemp); logfile.Write ("上次已抽取数据的位置 (%s=%ld)。\n" , starg.incfield, imaxincvalue); return true ; } bool writeincfile () { if (strlen (starg.incfield)==0 ) return true ; CFile File; if (File.Open (starg.incfilename,"w+" )==false ) { logfile.Write ("File.Open(%s) failed.\n" ,starg.incfilename); return false ; } File.Fprintf ("%ld" ,imaxincvalue); File.Close (); return true ; }
数据抽取的优化
第一个问题:现在给出的xml批量是1000一次,之前我们的理由是文件数目不能太大,否则不好处理,但也不绝对,就比如,如果数目比较小,我们使用一起插入,只需要先把表中的全部删掉,然后直接一锅端就好,但如果分成了多个文件,反而复杂,因为入库程序不知道我们有多少个文件,也不知道文件是否完整
第二个问题:我们现在的程序,自增字段是保存在logfile文件中,我们实际上可以保存到数据库中,不过也不是说保存在文件中不好,因为万一到了别的服务器,根本就没有数据库环境,就又会出现大麻烦,最好的办法就是都支持。
这里我们先修改储存的结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct st_arg { char connstr[101 ]; char charset[51 ]; char selectsql[1024 ]; char fieldstr[501 ]; char fieldlen[501 ]; char bfilename[31 ]; char efilename[31 ]; char outpath[301 ]; int maxcount; char starttime[52 ]; char incfield[31 ]; char incfilename[301 ]; char connstr1[101 ]; int timeout; char pname[51 ]; } starg;
增加了maxcount和connstr1
两点注意:
connstr1是我们自己的数据库、connstr是别人的数据库
剩下的说明文档,xml解析啥的,要准备改了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 if (strlen (starg.incfield)!=0 ){ for (int ii=0 ;ii<ifieldcount;ii++) if (strcmp (starg.incfield,strfieldname[ii])==0 ) { incfieldpos=ii; break ; } if (incfieldpos==-1 ){ logfile.Write ("递增字段名%s不在列表%s中。\n" ,starg.incfield,starg.fieldstr); return false ; } } if ((strlen (starg.incfilename) == 0 ) && (strlen (starg.connstr1) == 0 )){ logfile.Write ("incfilename和connstr1参数必须二选一 \n" ); return false ; }
之前设置一千行的地方,将一千替换为maxcount,并且有的地方注意限制maxcount必须大于0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 if (starg.maxcount == 0 ){ --> if ((starg.maxcount>0 ) && (stmt.m_cda.rpc%starg.maxcount == 0 ) ){ File.Fprintf ("</data>\n" ); logfile.Write ("生成文件%s(%d). \n" , strxmlfilename, stmt.m_cda.rpc%starg.maxcount); ---> if (starg.maxcount == 0 ) logfile.Write ("生成文件%s(%d). \n" , strxmlfilename, stmt.m_cda.rpc); else logfile.Write ("生成文件%s(%d). \n" , strxmlfilename, stmt.m_cda.rpc%starg.maxcount);
测试的时候,修改 里面的文字,另外需要删掉 再试
接下来增加,使得自增字段加入数据库功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 if (conn.connecttodb (starg.connstr,starg.charset)!=0 ){ logfile.Write ("connect database(%s) failed.\n%s\n" ,starg.connstr,conn.m_cda.message); return -1 ; } if (strlen (starg.connstr1)!=0 ) { if (conn1.connecttodb (starg.connstr1,starg.charset)!=0 ) { logfile.Write ("connect database(%s) failed.\n%s\n" ,starg.connstr1,conn1.m_cda.message); return -1 ; } logfile.Write ("connect database(%s) ok.\n" ,starg.connstr1); } logfile.Write ("connect database(%s) ok.\n" ,starg.connstr);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 bool readincfield () { imaxincvalue=0 ; if (strlen (starg.incfield) == 0 ) return true ; if (strlen (starg.connstr1) != 0 ){ sqlstatement stmt (&conn1) ; stmt.prepare ("select maxincvalue from T_MAXINCVALUE where pname=:1" ); stmt.bindin (1 ,starg.pname,50 ); stmt.bindout (1 ,&imaxincvalue); stmt.execute (); stmt.next (); } else { CFile File; if (File.Open (starg.incfilename,"r" )==false ) return true ; char strtemp[31 ]; File.FFGETS (strtemp, 30 ); imaxincvalue = atol (strtemp); } logfile.Write ("上次已抽取数据的位置 (%s=%ld)。\n" , starg.incfield, imaxincvalue); return true ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 bool writeincfield () { if (strlen (starg.incfield)==0 ) return true ; if (strlen (starg.connstr1) != 0 ){ sqlstatement stmt (&conn1) ; if (stmt.m_cda.rc == 1146 ){ conn1.execute ("create table T_MAXINCVALUE(pname varchar(50),maxincvalue numeric(15),primary key(pname))" ); conn1.execute ("insert into T_MAXINCVALUE values('%s',%ld)" ,starg.pname,imaxincvalue); conn1.commit (); return true ; } stmt.bindin (1 ,&imaxincvalue); stmt.bindin (2 ,starg.pname,50 ); if (stmt.execute ()!=0 ){ logfile.Write ("stmt.execute() failed.\n%s\n%s\n" ,stmt.m_sql,stmt.m_cda.message); return false ; } if (stmt.m_cda.rpc==0 ){ conn1.execute ("insert into T_MAXINCVALUE values('%s',%ld)" ,starg.pname,imaxincvalue); } conn1.commit (); } else { CFile File; if (File.Open (starg.incfilename,"w+" )==false ) { logfile.Write ("File.Open(%s) failed.\n" ,starg.incfilename); return false ; } File.Fprintf ("%ld" ,imaxincvalue); File.Close (); return true ; } }
学习总结
数据处理对象一般有几种类型。
网点信息,一般数据量不多
账户信息,数据量比较大,超过千万的级别
流水,数据量特别大,超过亿的级别
在我们这节课中,气象站点数据属于第一种,气象观测数据属于第三种,气象行业没有第二种数据
处理经验
对于第一个表,我们通常采用全量抽取,每次抽取全部的数据
第二个表,采用全量抽取,他有更新时间的字段,假设一小时更新一次,可以把抽取数据的条件设置为两个小时,肯定不会漏掉数据
第三个表,是自增字段,采用增量抽取的方法,每次抽取新增数据也很容易
可,这些都只是理想情况,实际项目开发,最根本的问题是数据源表的操作不规范
我们要注意的原则
七、数据入库子系统 数据入库的三种方式
如果只是获取数据,再传入数据中心,那么数据类型不同,就需要开发不同的入库代码,这样是十分繁琐的,因此我们想的是将获取到的数据都转化为不同的xml文件,然后通过数据入库模块来智能选择如何入库
目标
拓展-MySQL数据字典
INFORMATION_SCHEMA是信息数据库 ,其中保存着关于MySQL服务器所维护的所有其他数据库的信息。在INFORMATION_SCHEMA中,有数个只读表。它们实际上是视图 ,而不是基本表,因此,你将无法看到与之相关的任何文件 。
入库设计要求
我们的设计是这样的,可以理解一下理论是否能做到。
数据入库基本流程
声明参数
加载文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 bool loadxmltotable () { vxmltotable.clear (); CFile File; if (File.Open (starg.inifilename,"r" )==false ){ logfile.Write ("File.Open(%s) 失败。\n" ,starg.inifilename); return false ; } char strBuffer[501 ]; while (true ){ if (File.FFGETS (strBuffer,500 ,"<endl/>" )==false ) break ; memset (&stxmltotable,0 ,sizeof (struct st_xmltotable)); GetXMLBuffer (strBuffer,"filename" ,stxmltotable.filename,100 ); GetXMLBuffer (strBuffer,"tname" ,stxmltotable.tname,30 ); GetXMLBuffer (strBuffer,"uptbz" ,&stxmltotable.uptbz); GetXMLBuffer (strBuffer,"execsql" ,stxmltotable.execsql,300 ); vxmltotable.push_back (stxmltotable); } logfile.Write ("loadxmltotable(%s) ok.\n" ,starg.inifilename); return true ; }
查找文件 1 2 3 4 5 6 7 8 9 10 11 bool findxmltotable (char *xmlfilename) { for (int ii=0 ;ii<vxmltotable.size ();ii++){ if (MatchStr (xmlfilename,vxmltotable[ii].filename)==true ){ memcpy (&stxmltotable,&vxmltotable[ii],sizeof (struct st_xmltotable)); return true ; } } return false ; }
加载频率 我们可以思考一下loadxmltotable应该放在程序主流程的哪里,如果放在程序开头,意味着只加载一次,后面无法变动,如果直接放在while里,也意味着每次程序运行都要加载,过于频繁,因此我们可以定义一个计数器 。
1 2 3 4 5 6 7 8 9 10 11 12 bool _xmltodb(){ int counter=50 ; CDir Dir; while (true ){ if (counter++>30 ){ counter=0 ; if (loadxmltotable ()==false ) return false ; }
入库的参数文件会修改,但是修改的频率也不是很高。这样每循环三十次,再重新加载,就相对来说节省内存,又能及时加载,也是经典折中处理。
流程补充 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 bool _xmltodb(){ int counter=50 ; CDir Dir; while (true ){ if (counter++>30 ){ counter=0 ; if (loadxmltotable ()==false ) return false ; } if (Dir.OpenDir (starg.xmlpath,"*.XML" ,10000 ,false ,true )==false ){ logfile.Write ("Dir.OpenDir(%s) failed.\n" ,starg.xmlpath); return false ; } while (true ){ if (Dir.ReadDir ()==false ) break ; logfile.Write ("处理文件%s..." ,Dir.m_FullFileName); int iret=_xmltodb(Dir.m_FullFileName,Dir.m_FileName); if (iret==0 ){ logfile.WriteEx ("ok.\n" ); if (xmltobakerr (Dir.m_FullFileName,starg.xmlpath,starg.xmlpathbak)==false ) return false ; } if (iret==1 ){ logfile.WriteEx ("failed,没有配置入库参数。\n" ); if (xmltobakerr (Dir.m_FullFileName,starg.xmlpath,starg.xmlpatherr)==false ) return false ; } } break ; sleep (starg.timetvl); } return true ; }
备份文件 1 2 3 4 5 6 7 8 9 10 11 12 13 bool xmltobakerr (char *fullfilename,char *srcpath,char *dstpath) { char dstfilename[301 ]; STRCPY (dstfilename,sizeof (dstfilename),fullfilename); UpdateStr (dstfilename,srcpath,dstpath,false ); if (RENAME (fullfilename,dstfilename)==false ){ logfile.Write ("RENAME(%s,%s) failed.\n" ,fullfilename,dstfilename); return false ; } return true ; }
核心入库过程框架 请注意,这里返回值是int,是bool _xmltodb的子函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 int _xmltodb(char *fullfilename,char *filename){ if (findxmltotable (filename)==false ) return 1 ; CTABCOLS TABCOLS; if (TABCOLS.allcols (&conn,stxmltotable.tname)==false ) return 4 ; if (TABCOLS.pkcols (&conn,stxmltotable.tname)==false ) return 4 ; if (TABCOLS.m_allcount==0 ) return 2 ; return 0 ; }
新增结构体定义 1 2 3 4 5 6 7 struct st_columns { char colname[31 ]; char datatype[31 ]; int collen; int pkseq; };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class CTABCOLS {public : CTABCOLS (); int m_allcount; int m_pkcount; vector<struct st_columns> m_vallcols; vector<struct st_columns> m_vpkcols; char m_allcols[3001 ]; char m_pkcols[301 ]; void initdata () ; bool allcols (connection *conn,char *tablename) ; bool pkcols (connection *conn,char *tablename) ; };
allcols获取全部字段 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 bool CTABCOLS::allcols (connection *conn,char *tablename) { m_allcount=0 ; m_vallcols.clear (); memset (m_allcols,0 ,sizeof (m_allcols)); struct st_columns stcolumns ; sqlstatement stmt; stmt.connect (conn); stmt.prepare ("select lower(column_name),lower(data_type),character_maximum_length from information_schema.COLUMNS where table_name=:1" ); stmt.bindin (1 ,tablename,30 ); stmt.bindout (1 , stcolumns.colname,30 ); stmt.bindout (2 , stcolumns.datatype,30 ); stmt.bindout (3 ,&stcolumns.collen); if (stmt.execute ()!=0 ) return false ; while (true ){ memset (&stcolumns,0 ,sizeof (struct st_columns)); if (stmt.next ()!=0 ) break ; if (strcmp (stcolumns.datatype,"char" )==0 ) strcpy (stcolumns.datatype,"char" ); if (strcmp (stcolumns.datatype,"varchar" )==0 ) strcpy (stcolumns.datatype,"char" ); if (strcmp (stcolumns.datatype,"datetime" )==0 ) strcpy (stcolumns.datatype,"date" ); if (strcmp (stcolumns.datatype,"timestamp" )==0 ) strcpy (stcolumns.datatype,"date" ); if (strcmp (stcolumns.datatype,"tinyint" )==0 ) strcpy (stcolumns.datatype,"number" ); if (strcmp (stcolumns.datatype,"smallint" )==0 ) strcpy (stcolumns.datatype,"number" ); if (strcmp (stcolumns.datatype,"mediumint" )==0 ) strcpy (stcolumns.datatype,"number" ); if (strcmp (stcolumns.datatype,"int" )==0 ) strcpy (stcolumns.datatype,"number" ); if (strcmp (stcolumns.datatype,"integer" )==0 ) strcpy (stcolumns.datatype,"number" ); if (strcmp (stcolumns.datatype,"bigint" )==0 ) strcpy (stcolumns.datatype,"number" ); if (strcmp (stcolumns.datatype,"numeric" )==0 ) strcpy (stcolumns.datatype,"number" ); if (strcmp (stcolumns.datatype,"decimal" )==0 ) strcpy (stcolumns.datatype,"number" ); if (strcmp (stcolumns.datatype,"float" )==0 ) strcpy (stcolumns.datatype,"number" ); if (strcmp (stcolumns.datatype,"double" )==0 ) strcpy (stcolumns.datatype,"number" ); if ( (strcmp (stcolumns.datatype,"char" )!=0 ) && (strcmp (stcolumns.datatype,"date" )!=0 ) && (strcmp (stcolumns.datatype,"number" )!=0 ) ) continue ; if (strcmp (stcolumns.datatype,"date" )==0 ) stcolumns.collen=19 ; if (strcmp (stcolumns.datatype,"number" )==0 ) stcolumns.collen=20 ; strcat (m_allcols,stcolumns.colname); strcat (m_allcols,"," ); m_vallcols.push_back (stcolumns); m_allcount++; } if (m_allcount>0 ) m_allcols[strlen (m_allcols)-1 ]=0 ; return true ; }
pkcols获取指定字段同理
拼接SQL语句 实现insert和update功能,目的是组装好对应的mysql语句,然后存放到char strinsertsql[10241]和char strupdatesql[10241]中,方便
主要是用于数据入库的参数配置文件的uptbz参数使用,1为更新,2是不更新
1 2 3 4 5 6 7 8 <?xml version='1.0' encoding='utf-8'?> <xmltodb > <filename > ZHOBTCODE_*.XML</filename > <tname > T_ZHOBTCODE1</tname > <uptbz > 1</uptbz > <execsql > delete from T_ZHOBTCODE1</execsql > <endl /> <filename > ZHOBTMIND_*.XML</filename > <tname > T_ZHOBTMIND1</tname > <uptbz > 2</uptbz > <endl /> </xmltodb >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 void crtsql () { memset (strinsertsql,0 ,sizeof (strinsertsql)); memset (strupdatesql,0 ,sizeof (strupdatesql)); char strinsertp1[3001 ]; char strinsertp2[3001 ]; memset (strinsertp1,0 ,sizeof (strinsertp1)); memset (strinsertp2,0 ,sizeof (strinsertp2)); int colseq=1 ; for (int i=0 ;i<TABCOLS.m_vallcols.size ();i++){ if ( (strcmp (TABCOLS.m_vallcols[i].colname,"upttime" )==0 ) || (strcmp (TABCOLS.m_vallcols[i].colname,"keyid" )==0 ) ) continue ; strcat (strinsertp1,TABCOLS.m_vallcols[i].colname); strcat (strinsertp1,"," ); char strtemp[101 ]; if (strcmp (TABCOLS.m_vallcols[i].datatype,"date" )!=0 ) SNPRINTF (strtemp,100 ,sizeof (strtemp),":%d" ,colseq); else SNPRINTF (strtemp,100 ,sizeof (strtemp),"str_to_date(:%d,'%%%%Y%%%%m%%%%d%%%%H%%%%i%%%%s')" ,colseq); strcat (strinsertp2,strtemp); strcat (strinsertp2,"," ); colseq++; } strinsertp1[strlen (strinsertp1)-1 ]=0 ; strinsertp2[strlen (strinsertp2)-1 ]=0 ; SNPRINTF (strinsertsql,10240 ,sizeof (strinsertsql),\ "insert into %s(%s) values(%s)" ,stxmltotable.tname,strinsertp1,strinsertp2); if (stxmltotable.uptbz!=1 ) return ; for (int i=0 ;i<TABCOLS.m_vpkcols.size ();i++) for (int jj=0 ;jj<TABCOLS.m_vallcols.size ();jj++) if (strcmp (TABCOLS.m_vpkcols[i].colname,TABCOLS.m_vallcols[jj].colname)==0 ){ TABCOLS.m_vallcols[jj].pkseq=TABCOLS.m_vpkcols[i].pkseq; break ; } sprintf (strupdatesql,"update %s set " ,stxmltotable.tname); colseq=1 ; for (int i=0 ;i<TABCOLS.m_vallcols.size ();i++){ if (strcmp (TABCOLS.m_vallcols[i].colname,"keyid" )==0 ) continue ; if (TABCOLS.m_vallcols[i].pkseq!=0 ) continue ; if (strcmp (TABCOLS.m_vallcols[i].colname,"upttime" )==0 ){ strcat (strupdatesql,"upttime=now()," ); continue ; } char strtemp[101 ]; if (strcmp (TABCOLS.m_vallcols[i].datatype,"date" )!=0 ) SNPRINTF (strtemp,100 ,sizeof (strtemp),"%s=:%d" ,TABCOLS.m_vallcols[i].colname,colseq); else SNPRINTF (strtemp,100 ,sizeof (strtemp),"%s=str_to_date(:%d,'%%%%Y%%%%m%%%%d%%%%H%%%%i%%%%s')" ,TABCOLS.m_vallcols[i].colname,colseq); strcat (strupdatesql,strtemp); strcat (strupdatesql,"," ); colseq++; } strupdatesql[strlen (strupdatesql)-1 ]=0 ; strcat (strupdatesql," where 1=1 " ); for (int i=0 ;i<TABCOLS.m_vallcols.size ();i++){ if (TABCOLS.m_vallcols[i].pkseq==0 ) continue ; char strtemp[101 ]; if (strcmp (TABCOLS.m_vallcols[i].datatype,"date" )!=0 ) SNPRINTF (strtemp,100 ,sizeof (strtemp)," and %s=:%d" ,TABCOLS.m_vallcols[i].colname,colseq); else SNPRINTF (strtemp,100 ,sizeof (strtemp)," and %s=str_to_date(:%d,'%%%%Y%%%%m%%%%d%%%%H%%%%i%%%%s')" ,TABCOLS.m_vallcols[i].colname,colseq); strcat (strupdatesql,strtemp); colseq++; } }
绑定SQL语句参数 1 2 3 4 5 6 #define MAXCOLCOUNT 300 #define MAXCOLLEN 100 char strcolvalue[MAXCOLCOUNT][MAXCOLLEN+1 ]; sqlstatement stmtins,stmtupt; void preparesql () ;
连接数据库,将之前生成的sql语句用prepare方法,绑定在sqlstatement对象中,并跳过掉比如upttime、keyid、pkseq主键等字段,利用bindin函数绑定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 void preparesql () { stmtins.connect (&conn); stmtins.prepare (strinsertsql); int colseq=1 ; for (int i=0 ;i<TABCOLS.m_vallcols.size ();i++){ if ( (strcmp (TABCOLS.m_vallcols[i].colname,"upttime" )==0 ) || (strcmp (TABCOLS.m_vallcols[i].colname,"keyid" )==0 ) ) continue ; stmtins.bindin (colseq,strcolvalue[i],TABCOLS.m_vallcols[i].collen); colseq++; } if (stxmltotable.uptbz!=1 ) return ; stmtupt.connect (&conn); stmtupt.prepare (strupdatesql); colseq=1 ; for (int i=0 ;i<TABCOLS.m_vallcols.size ();i++){ if ( (strcmp (TABCOLS.m_vallcols[i].colname,"upttime" )==0 ) || (strcmp (TABCOLS.m_vallcols[i].colname,"keyid" )==0 ) ) continue ; if (TABCOLS.m_vallcols[i].pkseq!=0 ) continue ; stmtupt.bindin (colseq,strcolvalue[i],TABCOLS.m_vallcols[i].collen); colseq++; } for (int i=0 ;i<TABCOLS.m_vallcols.size ();i++){ if (TABCOLS.m_vallcols[i].pkseq==0 ) continue ; stmtupt.bindin (colseq,strcolvalue[i],TABCOLS.m_vallcols[i].collen); colseq++; } }
执行SQL语句 执行主要是插入和更新 操作。
解析XML,将内部语句存放在已绑定的输入变量,方便直接execute()执行命令,execute后全都是判断错误代码,也就是万一发生意外时,能在日志保存下痕迹。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 while (true ){ if (File.FFGETS (strBuffer,10240 ,"<endl/>" )==false ) break ; totalcount++; splitbuffer (strBuffer); if (stmtins.execute ()!=0 ){ if (stmtins.m_cda.rc==1062 ){ if (stxmltotable.uptbz==1 ){ if (stmtupt.execute ()!=0 ){ logfile.Write ("%s" ,strBuffer); logfile.Write ("stmtupt.execute() failed.\n%s\n%s\n" ,stmtupt.m_sql,stmtupt.m_cda.message); if ( (stmtupt.m_cda.rc==1053 ) || (stmtupt.m_cda.rc==2013 ) ) return 4 ; } else uptcount++; } } else { logfile.Write ("%s" ,strBuffer); logfile.Write ("stmtins.execute() failed.\n%s\n%s\n" ,stmtins.m_sql,stmtins.m_cda.message); if ( (stmtins.m_cda.rc==1053 ) || (stmtins.m_cda.rc==2013 ) ) return 4 ; } } else inscount++; } conn.commit (); return 0 ; }
解析XML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 void splitbuffer (char *strBuffer) { for (int i=0 ; i<TABCOLS.m_allcount; i++) memset (strcolvalue[i],0 ,TABCOLS.m_vallcols[i].collen+1 ); char strtemp[31 ]; for (int i=0 ; i<TABCOLS.m_vallcols.size (); i++){ if (strcmp (TABCOLS.m_vallcols[i].datatype,"date" )==0 ){ GetXMLBuffer (strBuffer,TABCOLS.m_vallcols[i].colname,strtemp,TABCOLS.m_vallcols[i].collen); PickNumber (strtemp,strcolvalue[i],false ,false ); continue ; } if (strcmp (TABCOLS.m_vallcols[i].datatype,"number" )==0 ){ GetXMLBuffer (strBuffer,TABCOLS.m_vallcols[i].colname,strtemp,TABCOLS.m_vallcols[i].collen); PickNumber (strtemp,strcolvalue[i],true ,true ); continue ; } GetXMLBuffer (strBuffer,TABCOLS.m_vallcols[i].colname,strcolvalue[i],TABCOLS.m_vallcols[i].collen); } }
完善与优化 问题一:我们首先观察,入库文件太单调了,正常的程序而言,应该反映出插入多少,删除多少这些虽然不必要但是也应该有的数据。
解决方案:在合适的位置,记录对应指标。(记得初始化)
问题二:
解决方案:
问题三:长时间没有数据文件处理
我们来梳理一下结构,一般来说,如果只开启一个隧道,那么可能就会出现数据堵塞的情况,造成延迟upttime,因此可以采取开启多条传输通道,但每种数据的特点不同,因此要分开处理,另外,也有可能数据库长时间未连接断开,所以这个判断语句应该放在while里定期检测,而不是放在开头只检测一次
问题四:字符串的大小和长度都是用宏处理,如果表的字段和长度大于定义的宏,那么就会出现内存溢出
的问题。
解决方案:动态内存处理
MAXCOLCOUNT调整为500个字段,(实际开发连300个字段基本上都没见到过),并将char二维数组变成char指针数组
在int _xmltodb中动态的分配内存(要记得使用之前,splitbuffer内初始化)
释放内存放在int _xmltodb的靠前部分,因为我们每次使用该函数,都需要将上次使用的痕迹清空,也就是把上次读取到的字段啥的都清空呗,并且指针也置为空。
最后一个地方就是进程的心跳。在程序中,每处理一次文件,增加一个心跳。
执行错误提示 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 if (iret==0 ){ logfile.WriteEx ("ok(%s,total=%d,insert=%d,update=%d).\n" ,stxmltotable.tname,totalcount,inscount,uptcount); if (xmltobakerr (Dir.m_FullFileName,starg.xmlpath,starg.xmlpathbak)==false ) return false ; } if ( (iret==1 ) || (iret==2 ) || (iret==5 ) ){ if (iret==1 ) logfile.WriteEx ("failed,没有配置入库参数。\n" ); if (iret==2 ) logfile.WriteEx ("failed,待入库的表(%s)不存在。\n" ,stxmltotable.tname); if (iret==5 ) logfile.WriteEx ("failed,待入库的表(%s)字段数太多。\n" ,stxmltotable.tname); if (xmltobakerr (Dir.m_FullFileName,starg.xmlpath,starg.xmlpatherr)==false ) return false ; } if (iret==3 ){ logfile.WriteEx ("failed,打开xml文件失败。\n" ); return false ; } if (iret==4 ){ logfile.WriteEx ("failed,数据库错误。\n" ); return false ; } if (iret==6 ){ logfile.WriteEx ("failed,执行execsql失败。\n" ); return false ; } } if (Dir.m_vFileName.size ()==0 ) sleep (starg.timetvl);
大量数据入库的方案 这个系统能够满足95%以上的业务需求,但业务总是复杂的,总有很多特殊的需求。这里举一个例子说明,车主服务。
然而公安局肯定不会提供他自己的数据库给你 连接,而是提供视图
未处理就是还没有交罚款的,这种数据有两个特点:
数据是视图,没有时间戳,也没有自增字段
数据量非常大,通常超百万
这种数据,怎么采撷,用数据抽取程序,每次采用全量抽取,每次放在一个文件中,再把文件传回来,那么,数据拿回来之后,如何入库?
因此我们还是采用和创建文件类似的方法,注意是删除表,不是删除表中的数据,这种方法也会有延迟,延迟在于删除到改名那0.00几秒,正常情况不会影响业务。
八、数据处理和统计 数据中心总体结构图
我们之前开发的TCP传输,FTP传输,数据抽取模块都是用于数据采集,为了解决从数据源取数据的问题,如果是xml或者json格式,直接入库,如果不是,则转化为xml和json,这个工作称为数据处理
。
数据入库到数据库中,可能要进行统计分析,再加工成新的业务产品,这个工作看业务需求。
数据处理的工作内容
不管怎么复杂,我们做的只有三步
数据统计的工作内容
九、数据同步子系统 mysql高可用
(18条消息) 什么是mysql的高可用_如何做到数据库高可用?_weixin_39959236的博客-CSDN博客
那么什么是数据库高可用?
高可用(High Availability)是系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间 。
如果一台系统能够不间断的提供服务,那么这台系统的可用性据说100%。那如果系统每运行100个时间单位,就会出现1个时间单位无法提供服务,那么该台系统的可用性是99%。
目前大部分企业的高可用目标是4个9,也就是99.99%,也就是允许这台系统的年停机时间为52.56分钟。
为实现系统高可用,其架构设计的核心准则是:冗余
。
系统需要全天候24销售不间断运行,则需要相应的冗余机制,以防某台机器宕掉时无法访问,而冗余则可以通过部署至少两台服务器构成一个集群实现服务高可用。数据库除了定期备份还需要实现冷热备份。甚至可以在全球范围内部署灾备数据中心。
容灾就等价于高可用?
有人说容灾所说的“灾难”指的大范围,高烈度的故障,例如火灾、地震、洪水、大范围的停电,所以往往还会有个限定词叫异地容灾。而高可用只要做到硬件冗余,实现单点故障时的业务接管就行了。这个观点只对了一半。
现如今,企业真正关心的“灾难”可以泛指非正常情况下的故障停机 ,而高可用则还应当应对由于IT运维工作等原因带来的计划内停机,因为无论是计划内还是计划外的停机都会对企业业务连续性造成损失。并且灾难故障往往是十年一遇甚至百年一遇,而维护停机工作才是无可避免经常会对业务连续性造成威胁的停机事件。
mysql的高可用没什么特别,如果数据量不大,一个master,一个slave(从属),如果读需求很大,那就多加几个slave,如果一个master不能满足写的需求,那就创造多个master,再带多个slave。
三点不足
虽然有第三方的软件可以解决部分问题,但是要给钱,我们可以自己做数据复制,也叫数据同步,弥补mysql高可用方案的不足。
mysql的高可用只是简单的提供多个副本,数据库中有很多个数据,很多个表,不管你是否需要,它都给你复制过去。
数据架构 在我们这个课程中,数据架构是这样的,上面那几个数据库可以称为核心数据库,负责数据的入库,统计,加工和管理,如果一个数据库忙不过来,那就增加几个,这也是最理想的解决方案。有多少数据就增加几个。
核心数据库采用一主一备的方案,是为了处理单点故障
单点故障 (英语:single point of failure,缩写 SPOF )是指 系统 中一点失效,就会让整个系统无法运作的部件 ,换句话说,单点故障即会整体故障。
下面那层数据库是应用数据库,就是把数据提供给别人的库,向其他的系统提供数据支撑服务。应用数据库没有主备之分,但是有冗余,如果某一个应用数据库出现了问题,切换到另一个就行了。
数据同步子系统负责把核心数据库中的数据同步到业务数据库中,数据同步子系统就是我们要开发的内容
这是我们这个系统所用的架构,红色的箭头代表mysql自带的数据复制功能,下面那些密密麻麻的箭头是我们的数据同步程序,对应用数据库来说,它不需要是某个库的副本,他更关心的是他服务的对象需要什么数据。比如说预报库的数据来自于ABC三个库,他需要什么就存什么,不需要就不存,再比如说,实时数据库,他虽然核心数据库的,但是他只有最近三天的,超过三天的都不需要存,他的特点是数据很齐全,访问的效率很高,但是数据的数量不多。
dederated引擎配置 Linux下MySQL开启Federated引擎方法 - MySQL数据库 - 亿速云 (yisu.com)
创建federated引擎,注意,表名字随意,字段只能比远程端那个表的字段少,属于被包含关系。另外一定要创建主键和唯一键,不然使用federated只会导致性能大幅度下降。
注意事项 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 1、FEDERATED 表可能会被复制到其他的slave数据库,你需要确保slave服务器也能够使用定义在connection中或mysql.servers表中的link的用户名/密码 连接上远程服务器。 2、远程服务器必须是MySQL数据库,更重要是,必须要兼容不同版本的mysql,兼不兼容试了才知道。 3、在访问FEDERATED表中定义的远程数据库的表前,远程数据库中必须存在这张表。 ***************************************最重要的第四条 4、FEDERATED不支持普通索引(只支持主键和唯一键的快速查找),就算加了查询条件,也会进行全局扫描,从远程服务器把全部的数据拿回来,在本地进行过滤,会造成数据库性能的大幅度下降,还会增加网络IO磁盘说的压力 *************************************** 5、FEDERATED表不支持ALTER TABLE语句或者任何DDL语句 -- 所有的 DDL 语句都会导致事务隐式提交,换句话说,当你在执行 DDL 语句前,事务就已经提交了。这就意味着带有 DDL 语句的事务将来没有办法 rollback。也就是ctrl + z 6、FEDERATED表不支持事务,就算支持,我们也不会使用它,因为远程事务问题很多,不稳定,oracle中也不会用。 7、本地FEDERATED表无法知道远程库中表结构的改变 -- 只是一个链接而已,不是相互奔赴,就好像这个世界上你不知道有多少人认识你一样 8、任何drop语句都只是对本地库的操作,不对远程库有影响,就只是删除本地的链接而已,与远程表无关
目标
1 2 3 4 5 6 7 8 9 10 11 12 13 create table LK_ZHOBTCODE1 ( obtid varchar(10) not null comment '站点代码', cityname varchar(30) not null comment '城市名称', provname varchar(30) not null comment '省名称', lat int not null comment '纬度,单位:0.01度', lon int not null comment '经度,单位:0.01度', height int not null comment '海拔高度,单位:0.1米', upttime timestamp not null comment '更新时间', keyid int not null auto_increment comment '记录编号,自动增长列', primary key (obtid), unique key ZHOBTCODE1_KEYID (keyid) )ENGINE=FEDERATED CONNECTION='mysql://root:123456@192.168.198.128:3306/mysql/T_ZHOBTCODE1' DEFAULT CHARSET=utf8;
注意:我是在同一台虚拟机上做实验,所以不能都放在3306端口,另外3307默认没有打开,因此需要输入指令nc -lp3307&
打开端口。
在打开指令之前,可以先输入netstat -an| grep 3307
检测是否打开,另外(18条消息) mysql开启多个端口_mysql单个实例开启多个端口_三金乐了的博客-CSDN博客
(18条消息) mysql多端口配置_mysql多端口配置及其启动方法_weixin_39866817的博客-CSDN博客
算了,还是放弃了,理解思路就行hhc
最后还是解决了,将mysql root 的localhost改为%就行
1 2 3 use mysql; update user set host = '%' where user ='root'; flush privileges;
全表刷新功能 只需要一行sql语句就能搞定,要按照这个写法写
分批刷新功能
两个注意点
分批操作的流程需要一个循环,在循环里面执行2、3步,直到全部的数据被处理完。
从远程表查询需要的数据,为什么不在federated表,原因有两个
federated不支持普通索引,如果同步的条件不是主键,也不是唯一键,就会进行全表扫描。
就算federated表支持同步索引,也没有直接访问远程表来得好,因为传给federated需要经过一次中转,肯定没有不中转好
不同步分批 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 if (starg.synctype==1 ){ logfile.Write ("sync %s to %s ..." ,starg.fedtname,starg.localtname); stmtdel.prepare ("delete from %s %s" ,starg.localtname,starg.where); if (stmtdel.execute ()!=0 ){ logfile.Write ("stmtdel.execute() failed.\n%s\n%s\n" ,stmtdel.m_sql,stmtdel.m_cda.message); return false ; } stmtins.prepare ("insert into %s(%s) select %s from %s %s" ,starg.localtname,starg.localcols,starg.remotecols,starg.fedtname,starg.where); if (stmtins.execute ()!=0 ){ logfile.Write ("stmtins.execute() failed.\n%s\n%s\n" ,stmtins.m_sql,stmtins.m_cda.message); connloc.rollback (); return false ; } logfile.WriteEx (" %d rows in %.2fsec.\n" ,stmtins.m_cda.rpc,Timer.Elapsed ()); connloc.commit (); return true ; }
如果要定义条件查询
有两个注意点,
一、为了解决federated表和本地表字段不同的问题,可以用两个while函数,一个用于federated表的查询,一个用于本地表的删除,实际项目中通常是字段相同的,但我们这个项目可以试试不同
二、系统时间可能存在读取数据的延迟,导致数据残缺。不过通常不分批同步数据量都很小(1w以下)所以无所谓,有两个解决办法,第一是写语句的时候就设置好时间
替换成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 if (connrem.connecttodb (starg.remoteconnstr,starg.charset) != 0 ){ logfile.Write ("connect database(%s) failed.\n%s\n" ,starg.remoteconnstr,connrem.m_cda.message); return false ; } char remkeyvalue[51 ]; sqlstatement stmtsel (&connrem) ; stmtsel.prepare ("select %s from %s %s" ,starg.remotekeycol,starg.remotetname,starg.where); stmtsel.bindout (1 ,remkeyvalue,50 ); char bindstr[2001 ]; char strtemp[11 ]; memset (bindstr,0 ,sizeof (bindstr)); for (int ii=0 ;ii<starg.maxcount;ii++){ memset (strtemp,0 ,sizeof (strtemp)); sprintf (strtemp,":%lu," ,ii+1 ); strcat (bindstr,strtemp); } bindstr[strlen (bindstr)-1 ]=0 ; char keyvalues[starg.maxcount][51 ]; stmtdel.prepare ("delete from %s where %s in (%s)" ,starg.localtname,starg.localkeycol,bindstr); for (int ii=0 ;ii<starg.maxcount;ii++){ stmtdel.bindin (ii+1 ,keyvalues[ii],50 ); } stmtins.prepare ("insert into %s(%s) select %s from %s where %s in (%s)" ,starg.localtname,starg.localcols,starg.remotecols,starg.fedtname,starg.remotekeycol,bindstr); for (int ii=0 ;ii<starg.maxcount;ii++){ stmtins.bindin (ii+1 ,keyvalues[ii],50 ); } int ccount=0 ; memset (keyvalues,0 ,sizeof (keyvalues)); if (stmtsel.execute ()!=0 ){ logfile.Write ("stmtsel.execute() failed.\n%s\n%s\n" ,stmtsel.m_sql,stmtsel.m_cda.message); return false ; } while (true ){ if (stmtsel.next ()!=0 ) break ; strcpy (keyvalues[ccount],remkeyvalue); ccount++; if (ccount==starg.maxcount){ if (stmtdel.execute ()!=0 ){ logfile.Write ("stmtdel.execute() failed.\n%s\n%s\n" ,stmtdel.m_sql,stmtdel.m_cda.message); return false ; } if (stmtins.execute ()!=0 ){ logfile.Write ("stmtins.execute() failed.\n%s\n%s\n" ,stmtins.m_sql,stmtins.m_cda.message); return false ; } logfile.Write ("sync %s to %s(%d rows) in %.2fsec.\n" ,starg.fedtname,starg.localtname,ccount,Timer.Elapsed ()); connloc.commit (); ccount=0 ; memset (keyvalues,0 ,sizeof (keyvalues)); PActive.UptATime (); } } if (ccount>0 ){ if (stmtdel.execute ()!=0 ){ logfile.Write ("stmtdel.execute() failed.\n%s\n%s\n" ,stmtdel.m_sql,stmtdel.m_cda.message); return false ; } if (stmtins.execute ()!=0 ){ logfile.Write ("stmtins.execute() failed.\n%s\n%s\n" ,stmtins.m_sql,stmtins.m_cda.message); return false ; } logfile.Write ("sync %s to %s(%d rows) in %.2fsec.\n" ,starg.fedtname,starg.localtname,ccount,Timer.Elapsed ()); connloc.commit (); } return true ;
增量同步数据模块 开发了刷新同步之后,增量同步只需在此基础改就行。首先,增量同步肯定是分批的,我们删掉不分批的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 bool _syncincrement(bool &bcontinue){ CTimer Timer; bcontinue=false ; if (findmaxkey ()==false ) return false ; char remkeyvalue[51 ]; sqlstatement stmtsel (&connrem) ; stmtsel.prepare ("select %s from %s where %s>:1 %s order by %s" ,starg.remotekeycol,starg.remotetname,starg.remotekeycol,starg.where,starg.remotekeycol); stmtsel.bindin (1 ,&maxkeyvalue); stmtsel.bindout (1 ,remkeyvalue,50 );
1 2 3 4 5 6 7 8 9 10 11 GetXMLBuffer (strxmlbuffer,"timetvl" ,&starg.timetvl);if (starg.timetvl<=0 ) { logfile.Write ("timetvl is null.\n" ); return false ; }if (starg.timetvl>30 ) starg.timetvl=30 ;GetXMLBuffer (strxmlbuffer,"timeout" ,&starg.timeout);if (starg.timeout==0 ) { logfile.Write ("timeout is null.\n" ); return false ; }if (starg.timeout<starg.timetvl+10 ) starg.timeout=starg.timetvl+10 ;
获取自增字段最大值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 bool findmaxkey () { maxkeyvalue=0 ; sqlstatement stmt (&connloc) ; stmt.prepare ("select max(%s) from %s" ,starg.localkeycol,starg.localtname); stmt.bindout (1 ,&maxkeyvalue); if (stmt.execute ()!=0 ){ logfile.Write ("stmt.execute() failed.\n%s\n%s\n" ,stmt.m_sql,stmt.m_cda.message); return false ; } stmt.next (); return true ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 bool bcontinue; while (true ){ if (_syncincrement(bcontinue)==false ) EXIT (-1 ); if (bcontinue==false ) sleep (starg.timetvl); PActive.UptATime (); } bool _syncincrement(bool &bcontinue){ ................... if (stmtsel.m_cda.rpc>0 ) bcontinue=true ; return true ; }
不启用FEDERATED引擎 直接从远程表提取到数据同步程序,后执行语句储存到本地表
在这里只实现增量功能用来举例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 bool _syncincrementex(bool &bcontinue) { CTimer Timer; bcontinue=false ; if (findmaxkey ()==false ) return false ; CCmdStr CmdStr; CmdStr.SplitToCmd (starg.localcols,"," ); int colcount=CmdStr.CmdCount (); char colvalues[colcount][TABCOLS.m_maxcollen+1 ]; sqlstatement stmtsel (&connrem) ; stmtsel.prepare ("select %s from %s where %s>:1 %s order by %s" ,starg.remotecols,starg.remotetname,starg.remotekeycol,starg.where,starg.remotekeycol); stmtsel.bindin (1 ,&maxkeyvalue); for (int ii=0 ;ii<colcount;ii++) stmtsel.bindout (ii+1 ,colvalues[ii],TABCOLS.m_maxcollen); char bindstr[2001 ]; char strtemp[11 ]; memset (bindstr,0 ,sizeof (bindstr)); for (int ii=0 ;ii<colcount;ii++){ memset (strtemp,0 ,sizeof (strtemp)); sprintf (strtemp,":%lu," ,ii+1 ); strcat (bindstr,strtemp); } bindstr[strlen (bindstr)-1 ]=0 ; sqlstatement stmtins (&connloc) ; stmtins.prepare ("insert into %s(%s) values(%s)" ,starg.localtname,starg.localcols,bindstr); for (int ii=0 ;ii<colcount;ii++){ stmtins.bindin (ii+1 ,colvalues[ii],TABCOLS.m_maxcollen); } if (stmtsel.execute ()!=0 ){ logfile.Write ("stmtsel.execute() failed.\n%s\n%s\n" ,stmtsel.m_sql,stmtsel.m_cda.message); return false ; } while (true ){ memset (colvalues,0 ,sizeof (colvalues)); if (stmtsel.next ()!=0 ) break ; if (stmtins.execute ()!=0 ){ logfile.Write ("stmtins.execute() failed.\n%s\n%s\n" ,stmtins.m_sql,stmtins.m_cda.message); return false ; } if (stmtsel.m_cda.rpc%1000 ==0 ){ connloc.commit (); PActive.UptATime (); } } if (stmtsel.m_cda.rpc>0 ) { logfile.Write ("sync %s to %s(%d rows) in %.2fsec.\n" ,starg.remotetname,starg.localtname,stmtsel.m_cda.rpc,Timer.Elapsed ()); connloc.commit (); bcontinue=true ; } return true ; }
学习总结
mysql触发器 1、触发器的概念
[MySQL触发器概念、原理与用法详解_Mysql_脚本之家 (jb51.net)](https://www.jb51.net/article/164675.htm#:~:text= 触发器(trigger)是MySQL提供给程序员和数据分析员来保证数据完整性的一种方法,它是与表事件相关的特殊的存储过程,它的执行不是由程序调用,也不是手工启动,而是由事件来触发,比如当对一个表进行操作(insert,delete, update)时就会激活它执行。.,——百度百科. 上面是百度给的触发器的概念,我理解的触发器的概念,就是你执行一条sql语句,这条sql语句的执行会自动去触发执行其他的sql语句,就这么简单。. 超简说明:sql1->触发->sqlN,一条sql触发多个sql.)
触发器(trigger)是MySQL提供给程序员和数据分析员来保证数据完整性的一种方法,它是与表事件相关的特殊的存储过程,它的执行不是由程序调用,也不是手工启动,而是由事件来触发,比如当对一个表进行操作(insert,delete, update)时就会激活它执行。——百度百科
上面是百度给的触发器的概念,我理解的触发器的概念,就是你执行一条sql语句,这条sql语句的执行会自动去触发执行其他的sql语句,就这么简单。
超简说明:sql1->触发->sqlN,一条sql触发多个sql
细节 为何需要避免删除操作?
delete物理删除既不能释放磁盘空间,而且会**产生大量的碎片**,导**致索引频繁断裂**,**影响**SQL执行计划的**稳定性**,同时,在碎片回收时,**会耗用大量的CPU,磁盘空间**,影响表的正常DML操作。
在业务代码层面,应该做逻辑标记删除 ,避免物理删除,为了实现归档需求,可以采用MSQL分区特性来实现,都是DDL操作,没有碎片产生。
我们这个程序需要避免删除操作,但是如果表不是我们设计的怎么办?
用触发器同步,创建操作日志表,在账户基本信息表创建触发器,把对这个表的各种操作记录在日志表中
采用触发器同步的效率比较高,最大的问题是要在远程数据库表上创建触发器,会增加负担,另外也会对业务系统产生影响,出了问题不好归责,但是如果两个数据库都是自己的,那完全可以用
第二个方法,增加一个程序,定期扫描远程表本地表,反正检查到不一样就删除。
第三个方法,就是数据抽取程序+数据入库程序
不足 第一个缺点不能说完全怪我们,因为大家都有这个困扰,第二个确实是我们的缺点,因为我们要去读取远程表的数据
mysql的binlog 使用binlog,可以让我们远程访问程序核实正确的时候只需要查询日志,不需要进入表中减缓系统运行速度。
一、初步了解binlog
mysql binlog详解 - Presley - 博客园 (cnblogs.com)
1、MySQL的二进制日志binlog可以说是MySQL最重要的日志,它记录了所有的DDL和DML语句(除了数据查询语句select),以事件形式记录,还包含语句所执行的消耗的时间,MySQL的二进制日志是事务安全型的。
a、DDL
—-Data Definition Language 数据库定义语言
主要的命令有create、alter、drop等,ddl主要是用在定义或改变表(table)的结构,数据类型,表之间的连接和约束等初始工作上,他们大多在建表时候使用。
b、DML
—-Data Manipulation Language 数据操纵语言
主要命令是slect,update,insert,delete,就像它的名字一样,这4条命令是用来对数据库里的数据进行操作的语言
2、mysqlbinlog常见的选项有一下几个:
a、–start-datetime:从二进制日志中读取指定等于时间戳或者晚于本地计算机的时间
b、–stop-datetime:从二进制日志中读取指定小于时间戳或者等于本地计算机的时间 取值和上述一样
c、–start-position:从二进制日志中读取指定position 事件位置作为开始。
d、–stop-position:从二进制日志中读取指定position 事件位置作为事件截至
3、一般来说开启binlog日志大概会有1%的性能损耗。
4、binlog日志有两个最重要的使用场景。
a、mysql主从复制 :mysql replication在master端开启binlog,master把它的二进制日志传递给slaves来达到master-slave数据一致的目的。
MySQL之间数据复制的基础 是二进制日志文件 (binary log file)。一台MySQL数据库一旦启用二进制日志后,其作为master,它的数据库中所有操作都会以“事件”的方式记录在二进制日志中,其他数据库作为slave通过一个I/O线程与主服务器保持通信,并监控master的二进制日志文件的变化,如果发现master二进制日志文件发生变化,则会把变化复制到自己的中继日志中,然后slave的一个SQL线程会把相关的“事件”执行到自己的数据库中,以此实现从数据库和主数据库的一致性,也就实现了主从复制。
b、数 据恢复**:通过mysqlbinlog工具来恢复数据。
binlog日志包括两类文件:
1)、二进制日志索引文件(文件名后缀为.index)用于记录所有的二进制文件。
2)、二进制日志文件(文件名后缀为.00000*)记录数据库所有的DDL和DML(除了数据查询语句select)语句事件。
十、数据管理子系统
数据清理是指:数据没有价值了,需要删除。
数据迁移是指:出于性能与内存的考虑,把价值没这么大的数据移动一个位置。
数据清理
注意事项 sqlstatement对象一个时间只能执行一条语句是mysql的缺点,别的数据库没有
代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 bool _deletetable(){ CTimer Timer; char tmpvalue[51 ]; sqlstatement stmtsel (&conn1) ; stmtsel.prepare ("select %s from %s %s" ,starg.keycol,starg.tname,starg.where); stmtsel.bindout (1 ,tmpvalue,50 ); char bindstr[2001 ]; char strtemp[11 ]; memset (bindstr,0 ,sizeof (bindstr)); for (int ii=0 ;ii<MAXPARAMS;ii++){ memset (strtemp,0 ,sizeof (strtemp)); sprintf (strtemp,":%lu," ,ii+1 ); strcat (bindstr,strtemp); } bindstr[strlen (bindstr)-1 ]=0 ; char keyvalues[MAXPARAMS][51 ]; sqlstatement stmtdel (&conn2) ; stmtdel.prepare ("delete from %s where %s in (%s)" ,starg.tname,starg.keycol,bindstr); for (int ii=0 ;ii<MAXPARAMS;ii++) stmtdel.bindin (ii+1 ,keyvalues[ii],50 ); int ccount=0 ; memset (keyvalues,0 ,sizeof (keyvalues)); if (stmtsel.execute ()!=0 ){ logfile.Write ("stmtsel.execute() failed.\n%s\n%s\n" ,stmtsel.m_sql,stmtsel.m_cda.message); return false ; } while (true ){ memset (tmpvalue,0 ,sizeof (tmpvalue)); if (stmtsel.next ()!=0 ) break ; strcpy (keyvalues[ccount],tmpvalue); ccount++; if (ccount==MAXPARAMS){ if (stmtdel.execute ()!=0 ){ logfile.Write ("stmtdel.execute() failed.\n%s\n%s\n" ,stmtdel.m_sql,stmtdel.m_cda.message); return false ; } ccount=0 ; memset (keyvalues,0 ,sizeof (keyvalues)); PActive.UptATime (); } } if (ccount>0 ){ if (stmtdel.execute ()!=0 ){ logfile.Write ("stmtdel.execute() failed.\n%s\n%s\n" ,stmtdel.m_sql,stmtdel.m_cda.message); return false ; } } if (stmtsel.m_cda.rpc>0 ) logfile.Write ("delete from %s %d rows in %.02fsec.\n" ,starg.tname,stmtsel.m_cda.rpc,Timer.Elapsed ()); return true ; }
数据迁移 仅仅只多了中间那个步骤, 再删除以前先备份
1 2 3 sqlstatement stmtins (&conn2) ; stmtins.prepare ("insert into %s(%s) select %s from %s where %s in (%s)" ,starg.dsttname,TABCOLS.m_allcols,TABCOLS.m_allcols,starg.srctname,starg.keycol,bindstr);
这个每批次的数量会根据情况改变,例如,如果迁移表中有BLOB字段,那么每批迁移数就不能太多,因为BLOB字段占用的空间可能会很大。如果有可能会出现唯一键冲突,那么会导致一批次都迁移失败,所以有的时候会考虑一批只传送一个数据,出错了就不理他,写日志继续迁移其他的记录。
十一、Oracle数据库开发
Oracle用户的DBA登录sqlplus / as sysdba
用oracle用户登录,执行lsnrctl start
启动网络监听服务,执行dbstart
启动数据库系统。
用oracle用户登录,执行lsnrctl stop
关闭网络监听服务,执行dbshut
关闭数据库系统。
配置数据库地址
vi /oracle/home/network/admin/tnsnames.ora
oci头文件
$ORACLE_HOME/rdbms/public
mysql和oracle的区别 概念
其他区别:用到时,网上自行查找工具就行了
MySQL可以称为数据库服务,在一个服务中,可以创建多个数据库,在多个数据库中再创建表,索引,视图等对象。
Oracle不用数据库这个名词,用实例,在一个实例中可以创建多个用户,在用户中再创建表,索引,视图等对象
数据类型
MySQL中自增字段只有一个,并且还需要设置为唯一键,Oracle没有这个限制,一个表可以有多个自增字段,甚至没有自增字段,可以采用序列生成器
,Oracle也不要求把自增字段设置为唯一键,但我们一般也会设置。
Oracle开发基础
connection和sqlstatement基本上和mysql没啥区别。
错误代码也是一一对应就是了不需要记忆
但是注意事项。
Oracle故障排除的方法 如果对方服务器没有启动,网络不通,没有开通防火墙,会提示无法连接目标主机
,如果对方程序已经启动,并且开启了防火墙,那么就会提示无监听程序
,也就是说对方的监听服务没有启动。
如果可以telnet xxx.xxx.xxx.xxx port(1521) 说明网络没问题,防火墙也没问题,这个时候如果提示监听程序当前无法识别连接描述符所给出的SID
, 也就是说你想连接的数据库还没有启动,用dbstart启动。如果这样还不行,说明数据库的SID设置那里出错了,指定的SID根本就不存在
用户和权限管理基本知识
J:\11Projectc++\课程文档(1)\oracle数据库\28.Oracle用户和权限管理.docx
序列生成器基本知识
J:\11Projectc++\课程文档(1)\oracle数据库\11.Oracle序列生成器.docx
Oracle双引号单引号
(18条消息) Oracle数据库中单引号’ ‘ 和双引号” “的区别_Ninewind的博客-CSDN博客_数据库中单引号是什么意思
呜呜呜呜呜呜,利用PowerDesigner造表的时候一定要注意,引号要去掉啊!!!!这点差点把我干碎!
在Oracle数据库中,单引号’ ‘和双引号” “两者都是可以表示字符串的,但是在使用时会有所区别。
在双引号” “中,一般在如下场合使用
表示其内部的字符串严格区分大小写 (比如用作字段别名时区分大小写)
用于特殊字符或关键字 (比如包含空格,#或&时)
不受标识符规则限制
会被当成一个列来处理
当出现在to_char的格式字符串中时,双引号有特殊的作用,就是将非法的格式符包装起
而在单引号’ ‘中,一般在如下场合使用
表示字符串常量 (比如用于条件限定时where=’aa’,单引号用于条件限定时对大小写敏感)
字符串中的双引号仅仅当作一个字符串”处理,可以在单引号’ ‘中使用双引号”
如果字符串常量中包含了单引号’ ‘,那么需要使用两个单引号 ‘’ 表示一个单引号常量
数据入库子系统修改 从MySQL的版本改过来大致这样。
这个错误代码应该熟记(mysql是1062)
MYSQL版本,keyid字段无需处理,Oracle版本,keyid需要处理,upttime仍然不需要。
在我们这个项目有个约定,序列名和表名除了前缀,其他是一样的,所以我们可以用SEQ_序列名
的方式来得到序列名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if (strcmp (TABCOLS.m_vallcols[ii].colname,"keyid" )==0 ) { SNPRINTF (strtemp,100 ,sizeof (strtemp),"SEQ_%s.nextval" ,stxmltotable.tname+2 ); } else { if (strcmp (TABCOLS.m_vallcols[ii].datatype,"date" )!=0 ) SNPRINTF (strtemp,100 ,sizeof (strtemp),":%d" ,colseq); else SNPRINTF (strtemp,100 ,sizeof (strtemp),"to_date(:%d,'yyyymmddhh24miss')" ,colseq); colseq++; }
时间格式修改:
将mysql这种讨厌的格式
改为这样的格式:
另外就是SQL语句中的now()
改为sysdate
,Oracle没有now()
数据清理子系统修改
数据清理不用考虑2、3、4三个问题,不过需要注意还有更多需要修改的地方。
就比如mysql需要为每一种操作都创造一个数据库连接,否则会串线,Oracle就可以兼容,实现一个对象操纵多种命令。
Oracle不需要MAXPARMS这个宏,我们直接定义就好了。
我们直接使用,这样的效率不是最高的,为什么这么说
J:\11Projectc++\课程文档(1)\oracle数据库\17.Oracle伪列.docx
可以看看Oracle的伪列,非常重要
我们只需要将查找的条件keycol中的keyid
替换为rowid
就行,rowid是直接记录了这条记录在硬盘里的物理位置,肯定比任何索引都来的快,不过他是oracle所特有,用在别的数据库会有兼容问题,并且rowid不是固定的,会随着资源的移动而发生变化。
数据迁移子系统修改
数据抽取子系统修改
如果Mysql语句有语法错误,prepare会返回错误提示,但是oracle不会。
为了程序兼容性考虑,不要去判断prepare的返回值,错误代码943那行语句不能放在执行之前,应该在执行之后判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 if (strlen (starg.connstr1)!=0 ){ sqlstatement stmt (&conn1) ; stmt.prepare ("update T_MAXINCVALUE set maxincvalue=:1 where pname=:2" ); stmt.bindin (1 ,&imaxincvalue); stmt.bindin (2 ,starg.pname,50 ); if (stmt.execute ()!=0 ) { if (stmt.m_cda.rc==942 ) { conn1.execute ("create table T_MAXINCVALUE(pname varchar2(50),maxincvalue number(15),primary key(pname))" ); conn1.execute ("insert into T_MAXINCVALUE values('%s',%ld)" ,starg.pname,imaxincvalue); conn1.commit (); return true ; } else { logfile.Write ("stmt.execute() failed.\n%s\n%s\n" ,stmt.m_sql,stmt.m_cda.message); return false ; }
数据同步子系统修改 Oracle并没有federated引擎,但是有更强大的DBlink,关于它的使用可以看这篇文章。
J:\11Projectc++\课程文档(1)\oracle数据库\20.Oracle数据库链路.docx
数据库链路(database link),简称dblink,它是一个通道,是本地数据库与远程数据库之间的通道,通过dblink,在本地数据库中可以直接访问远程数据库的对象。
dblink不是应用程序与数据库之间的通道,而是数据库之间的通道
应用经验
dblink的知识很容易掌握,用dblink访问远程数据库的对象很方便,但是,如果在程序中采用dblink对远程数据库的表进行增、删、改、查操作时一定要遵守一个原则:尽可能不要产生远程事务,因为数据库对远程的事务难以控制 ,也就是说,尽可能不要对远程数据库的表进行增、删、改操作,查询是没有问题的。
先创建dblink
1 2 3 4 5 6 7 8 9 10 sqlplus / as sysdba SQL > grant create database link to qxidc;#SQL > grant create database link to scott; # 原本应该给远程服务器的这个授权权限 授权成功。 SQL > exit;sqlplus scott/ tiger SQL > create database link db128 connect to qxidc identified by qxidcpwd using 'snorcl11g_128' ;数据库链接已创建。
通过dblink访问远程数据库的权限是由dblink所采用的用户决定的,就像你拿着别人的员工卡进入公司大楼一样。
就比如qxidc用户没有访问这个表的权限,提示视图不存在,其实是他无法看到而已。
另外一台虚拟机上用dblink访问也是一样的意思
syncupdate_oracle.cpp、syncincrement_oracle.cpp解决方案,仅需要修改这两处
1 2 3 4 5 6 #define MAXPARAMS 256
对于syncincrementex_oracle.cpp而言,存在MAXPARAMS宏的问题,这个程序取出远程表的数据放入内存中,再插入本地表,需要绑定输入和输出变量,只要绑定变量,肯定会涉及到绑定参数个数最大值 的问题,不过这个问题,也只是对mysql版本,因为oracle封装的方法,无需限制有多大,可以不需要这个宏。
第二个问题,在绑定语句中,如果有时间字段,需要在程序中先转化为字符串,插入本地表的时候,再用to_date将字符串转化为时间,这样是很麻烦的,一种解决方法是数据库的时间缺省方式。
1 2 export NLS_DATE_FORMAT='yyyy-mm-dd hh24:mi:ss'
我们来做一个测试:采用DBlink和不采用DBlink的增量同步程序
这是Dblink的程序maxcount采用1、10、100的情况
因此我们得出结论,Dblink和批量处理的效率是非常高的,比FEDERATED的效率要高得多。
数据库集群方案 作为一个能搭建数据中心的程序员,数据库集群是避不开的问题,我们作为程序员,需要了解概念和原理,不需要动手实践。
RAC 每个结点都安装了oracle数据库和操作系统,他们共享存储,共享存储有容错机制,稳定性比服务器都要好很多,也很贵,如果一般结点坏了,不影响系统工作,共享存储坏了就玩完了。
上面是单实例的数据库,服务器发生故障的时候,客户端的connection会断开,但是对于RAC集群来说,他能使得并列的服务端,接管错误的那个服务端的connection,保证了持续连接,对应用程序来说是没有感觉的,业务也不会受到影响。RAC是高可用 的解决方案,不是高性能的解决方案,他能保持服务器永远在线(足够多结点),但是对性能没有提升,因为RAC共享一个存储设备,存储设备的性能决定了整体性能。
虽然只有一份文件,不能提高读写效率,但是却有办法提高查询效率,因为每个结点都有可能能保存账本的信息,直接反馈给客户端。这种处理方法对某些行业非常重要,比如说银行,证券,便利等行业,还有政府部门。它的代价也显而易见,烧硬件。IBM3850 是一个最好的选择,一般来说5w块钱的就可以了。如果觉得这个不够好,可以试试IBM小型机。用它的分区就可以了,每个分区相当于一台独立的服务器。
软件方面也特别的昂贵,这么说吧,搭建一个RAC要100w,RAC的服务端可以搭建很多,但实际中一般两个就够了,再多也没啥太大的意义。
RAC把数据写入共享内存时,多个节点之间需要协调,所以写速度低一些,不如单实例的数据库,读就因为都有备份,所以快一些。MYSQL也有RAC的功能,但是很脆弱,在几十万的硬件上运行MYSQL也会让人难以理解。
Data Guard Data Guard是Oracle自带的集群方案,类似于MySQL的主从复制,只不过这里是Primary site的数据,定期保存到Standby site,对应MySQL的master slave。
Data Guard的,primary写,standby读,是可以做的,但这样也显得有些笨,Oracle有更好的办法
OGG OGG主要有三个进程,源端的数据抽取进程,网络的文件传输进程,目标端的数据复制进程。最慢的地方就是将解析的数据插入目标端这个步骤,插入需要一点点来,确实是没办法的事情。
OGG vs 数据同步子系统
硬件配置 有可能面试会被问到。
存储设备又叫存储服务器,它的容量一般都很大,具体看业务需求,EMC是专门做这个的公司,比IBM做得要好
IBM3850+EMC是一个方案,IBM P750小型机分成四个区也是一个方案,一个区的性能比IBM3850还要好。如果用的是小型机,操作系统肯定是AIX,是UNIX的一种。
备份的服务器(Standby)不用太好,3650都可以,价格在2w多,内存也不用大,64G足够。
一个槽可以挂一个硬盘。
Oracle DBA 这是一个专门的岗位
Oracle新特性 inmemory不能提升写数据的性能,但是能提升读数据(几十倍)。Oracle12才有的
可能我们以后会见到这个框架,但如果有Oracle就不需要这么做了,新版本的Oracle自带这个功能,不再需要redis做缓存,使用起来也更方便。
区块链相关知识
等等等等……………….
MySQL何去何从
PostgreSQL及更多 它的性能比mysql强大的多,但因为没人维护,所以没人敢用
十二、linux线程 线程与进程 对程序员来说,调用进程和调用线程的代码不一样。但是,操作系统底层,都是调用同一个类层函数clone
,把进程复制一份。对类和函数来说,如果创建的是进程,还需要复制地址空间,如果创建的是线程,就不复制地址空间,让他和原来的进程共享地址空间。
注意:所谓的多线程都是指同一个进程下的多个线程。
线程的优缺点
学习任务
创建简单线程
提前说明,在这里我们把main函数叫做主线程,或者主进程。被创建的线程叫做线程或者子线程,子线程运行的函数叫线程主函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> void *thmain (void *arg) ; int main (int argc, char *argv[]) { pthread_t thid = 0 ; if (pthread_create (&thid, NULL , thmain, NULL ) != 0 ){ printf ("pthread_create failed.\n" ); exit (-1 ); } printf ("join...\n" ); pthread_join (thid, NULL ); printf ("join ok.\n" ); } void *thmain (void *arg) { for (int i = 0 ; i < 5 ; i++){ sleep (1 ); printf ("pthmain sleep(%d) ok.\n" , i+1 ); } }
我们要查看进程运行,先查看进程编号这里就是:ps -ef |grep Test01
找到./test01
使用ps -Lf 19526
来查看,上面那个的LWP和PID一样,所以他是主线程,下面那个是子线程
在多线程程序中,他们的LWP(PCB)是不同的,但是他们的地址空间是一样的
线程非正常终止 在多线程中,切记主线程不能退出,因为他们同处一室,如果他提前退出了,子线程没有机会把该干的事干完,如果主线程实在没事干,可以就在主线程中join()等待子线程结束
对于第三种情况,想要表达的重点是:
Core dump 当程序运行的过程中异常终止或崩溃 ,操作系统会将程序当时的内存状态记录下来 ,保存在 一个文件 中,这种行为 就叫做Core Dump(中文有的翻译成“核心转储”)。我们可以认为 core dump 是“内存快照”,但实际上,除了内存信息之外,还有些关键的程序运行状态也会同时 dump 下来,例如寄存器信息(包括程序指针、栈指针等)、内存管理信息、其他处理器和操作系统状态和信息。core dump 对于编程人员诊断和调试程序是非常有帮助的,因为对于有些程序错误是很难重现的,例如指针异常,而 core dump 文件可以再现程序出错时的情景。
终止线程的三种办法
return 0 代表的是NULL,本身就是地址,所以return 0可以直接写,return别的需要转换为void *地址
任意一个线程都可以。
exit里面放的值效果和return一样,如果不是写0,范回别的都需要提前转变量类型
既然这样,那么第一种和第三种方法又有什么区别呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void fun1 () { return ;}void *thmain1 (void *arg) { for (int ii=0 ;ii<5 ;ii++) { var=ii+1 ; sleep (1 ); printf ("pthmain1 sleep(%d) ok.\n" ,var); if (ii==2 ) fun1 (); } } void fun2 () { pthread_exit (0 );}void *thmain2 (void *arg) { for (int ii=0 ;ii<5 ;ii++) { sleep (1 ); printf ("pthmain2 sleep(%d) ok.\n" ,var); if (ii==2 ) fun2 (); } }
答案便是,如果在子线程中,exit能终止这个线程,return只会又范回这个线程,这个一般程序的exit(0)与return 0一样的道理。
线程参数的传递
对于第一个问题:
为什么会出现的原因,可以归结于,先创建的线程不一定先运行。
我们可以考虑采用sleep来使得结果不再紊乱
但是实际开发中,不可能采用sleep这种方法,会被别人笑话。TAT
正确的方法是,创建线程的时候,把create的第四个参数传递给线程主函数。
强制转换 在下图中,前四行是用本来存放地址的pv存放了ii的值,也就是pv输出即0xa(10)。后三行是用本来存放数字的jj存放地址的值,并且由于指针占用内存空间八字节,所以不允许直接转换为int,编译器只允许小转大,对此我们可以采用先转化为long,再转化为int的方法处理。
在日常开发中一般不会使用,但是在多线程中却常常使用。
注意品位下面的强制转换。我们传入的不是地址,而是整数类型值的地址,也就是类似于0x1,0x2这种,如果直接传var的地址,如果直接通过参数传入是不正确的,本质上和全局变量var别无二致。
这个程序类似于已经完成了前四个目标
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> void *thmain1 (void *arg) ; void *thmain2 (void *arg) ; void *thmain3 (void *arg) ; void *thmain4 (void *arg) ; void *thmain5 (void *arg) ; int var;int main (int argc,char *argv[]) { pthread_t thid1=0 ,thid2=0 ,thid3=0 ,thid4=0 ,thid5=0 ; var=1 ; if (pthread_create (&thid1,NULL ,thmain1,(void *)(long )var)!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); } var=2 ; if (pthread_create (&thid2,NULL ,thmain2,(void *)(long )var)!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); } var=3 ; if (pthread_create (&thid3,NULL ,thmain3,(void *)(long )var)!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); } var=4 ; if (pthread_create (&thid4,NULL ,thmain4,(void *)(long )var)!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); } var=5 ; if (pthread_create (&thid5,NULL ,thmain5,(void *)(long )var)!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); } printf ("join...\n" ); pthread_join (thid1,NULL ); pthread_join (thid2,NULL ); pthread_join (thid3,NULL ); pthread_join (thid4,NULL ); pthread_join (thid5,NULL ); printf ("join ok.\n" ); } void *thmain1 (void *arg) { printf ("var1=%d\n" ,(int )(long )arg); printf ("线程1开始运行。\n" ); } void *thmain2 (void *arg) { printf ("var2=%d\n" ,(int )(long )arg); printf ("线程2开始运行。\n" ); } void *thmain3 (void *arg) { printf ("var3=%d\n" ,(int )(long )arg); printf ("线程3开始运行。\n" ); } void *thmain4 (void *arg) { printf ("var4=%d\n" ,(int )(long )arg); printf ("线程4开始运行。\n" ); } void *thmain5 (void *arg) { printf ("var5=%d\n" ,(int )(long )arg); printf ("线程5开始运行。\n" ); }
如何正确传递地址参数 如果要给线程传地址,一定要保证给每一个线程传不同的地址,而不能都传同一个地址。
并且要注意,如果我们新创建了很多的内存空间,那么一定要在子函数中把这块区域关闭。如果在主函数关闭,那就和在主函数关闭程序一个道理,根本无法判断子函数是否执行完!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int *var1=new int ; *var1=1 ;if (pthread_create (&thid1,NULL ,thmain1,var1)!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); }int *var2=new int ; *var2=2 ;if (pthread_create (&thid2,NULL ,thmain2,var2)!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); }int *var3=new int ; *var3=3 ;if (pthread_create (&thid3,NULL ,thmain3,var3)!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); }int *var4=new int ; *var4=4 ;if (pthread_create (&thid4,NULL ,thmain4,var4)!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); }int *var5=new int ; *var5=5 ;if (pthread_create (&thid5,NULL ,thmain5,var5)!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); }
传递多个参数 线程的参数,如果传值,只能传一个,如果传地址,传入一个包含多个参数的结构体的地址,那就没问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> void *thmain (void *arg) ; struct st_args { int no; char name[51 ]; }; int main (int argc,char *argv[]) { pthread_t thid=0 ; struct st_args *stargs =new struct st_args; stargs->no=15 ; strcpy (stargs->name,"测试线程" ); if (pthread_create (&thid,NULL ,thmain,stargs)!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); } printf ("join...\n" ); pthread_join (thid,NULL ); printf ("join ok.\n" ); } void *thmain (void *arg) { struct st_args *pst = (struct st_args *)arg; printf ("no=%d,name=%s\n" ,pst->no,pst->name); delete pst; printf ("线程开始运行。\n" ); }
线程退出状态 实际开发中其实我们往往不关心这个,但是作为学习我们也必须了解,通常子线程的退出就是伴随着join函数的终止。我们先一起来了解一下join函数
join函数的第二个变量是一个二级指针,也就是指针变量的地址
。还记得我们的thmain函数返回值类型是void *吗?这个就是指,指向这个类型的指针。也就是能取得返回值地址的指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> void *thmain (void *arg) ; struct st_ret { int retcode; char message[1024 ]; }; int main (int argc,char *argv[]) { pthread_t thid=0 ; if (pthread_create (&thid,NULL ,thmain,NULL )!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); } printf ("join...\n" ); struct st_ret *pst =0 ; pthread_join (thid,(void **)&pst); printf ("retcode=%d,message=%s\n" ,pst->retcode,pst->message); delete pst; printf ("join ok.\n" ); } void *thmain (void *arg) { printf ("线程开始运行。\n" ); struct st_ret *ret =new struct st_ret; ret->retcode=1121 ; strcpy (ret->message,"测试内容。" ); pthread_exit ((void *)ret); }
线程资源的回收
先来复习复习进程资源的回收
线程未分离
我们先让它sleep10s,也就是子线程都执行完以后再join
1 2 3 4 5 6 7 8 9 10 11 12 13 if (pthread_create (&thid1,NULL ,thmain1,NULL )!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); } if (pthread_create (&thid2,NULL ,thmain2,NULL )!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); } sleep (10 ); void *ret; printf ("join...\n" ); int result=0 ; result=pthread_join (thid2,&ret); printf ("thid2 result=%d,ret=%ld\n" ,result,ret); result=pthread_join (thid1,&ret); printf ("thid1 result=%d,ret=%ld\n" ,result,ret); ret=0 ; result=pthread_join (thid2,&ret); printf ("thid2 result=%d,ret=%ld\n" ,result,ret); result=pthread_join (thid1,&ret); printf ("thid1 result=%d,ret=%ld\n" ,result,ret); printf ("join ok.\n" );
可以发现,函数仍然能捕捉到ret,也就是证明了资源并没完全退出,和租房子的确也是一个道理,但是第二次join就不行了,类似于第一次join就把子函数赶走了。
线程分离 主要用这两种方法分离。
pthread_detach() 只有一个参数,就把线程名输进去就可以了,大概意思就是指,用了这个不需要用join回收了,并且只有返回值为0即函数执行成功。
这个函数可以放在主函数中(需要留给足够的时间让子进程执行完)
也可以放在线程的主函数中,这个时候用pthread_self()
得到自己的ID
实际开发中更倾向于放在线程主函数中,因为更简单。
设置线程属性 太麻烦了,所以基本不用
阻塞 偶尔也会用到。tryjoin和join用法一样,不过,如果子线程没有终止,他不会等待,他立即返回。下面那个就是限制多久没终止,就返回。
线程清理函数 入栈和出站必须成对出现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void *thmain (void *arg) { pthread_cleanup_push (thcleanup1,NULL ); pthread_cleanup_push (thcleanup2,NULL ); pthread_cleanup_push (thcleanup3,NULL ); for (int ii=0 ;ii<3 ;ii++) { sleep (1 ); printf ("pthmain sleep(%d) ok.\n" ,ii+1 ); } pthread_cleanup_pop (3 ); pthread_cleanup_pop (2 ); pthread_cleanup_pop (1 ); }
然后在thcleanup1,2,3中释放资源。
只要清理函数已经入栈了,那么肯定会执行。
现在我们来研究一下这个清理函数的参数
execute
的取值如果为0,表示让这个函数出栈,并且不执行,反之传入别的任意值,都是执行
进程终止函数 声明
下面是 atexit() 函数的声明。
1 int atexit(void (*func)(void))
参数
返回值
如果函数成功注册,则该函数返回零,否则返回一个非零值。
实例
下面的实例演示了 atexit() 函数的用法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <stdio.h> #include <stdlib.h> void functionA () { printf ("这是函数A\n" ); } int main () { atexit (functionA ); printf ("启动主程序...\n" ); printf ("退出主程序...\n" ); return (0 ); }
让我们编译并运行上面的程序,这将产生以下结果:
线程的取消
取消不意味着终止,只是取消这次执行操作,线程并未结束。
使用方法很简单。
对于取消状态而言,其实没啥用,因为只有取消和不取消两种选择,缺省是取消,所以不用管它
另外,我们还可以设置线程的取消方式
DEFERRED是延迟取消,就你指定多久之后取消,可以理解为,你告诉它你该取消了,它说它知道了,但是他要运行到下一个能取消的地方才取消。ASYNCHRONOUS是立即取消(异步好诶)线程在任何时候都可以被取消
那么什么是取消点呢?我们来看看帮助文档(man 7 pthreads
)后按/points搜索定位
只要线程的代码中出现了这一堆(未显示完)函数,则叫做取消点
在实际开发中,如果线程中的代码没有取消点,那我们可以调用下面这个函数设置取消点,这是规范的做法。
应该这样
线程和信号 不管是进程还是线程,信号都比较复杂,可对于我们的开发而言,这个东西需要掌握的知识是比较简单的。
对于信号的执行,如果对同一个信号有多个执行函数,那么我们以最后被执行的那串代码 为准。
进程送达函数 pthread_kill():向指定的函数发送信号,和多进程的相似
信号的更多知识
线程安全
什么是线程安全,你真的了解吗? - 知乎 (zhihu.com)
什么是线程安全? 既然是线程安全问题,那么毫无疑问所有的隐患都是出现在多个线程访问的情况下产生的,也就是我们要确保在多条线程访问的时候,我们的程序还能按照我们预期的行为去执行,我们看一下下面的代码。
1 2 3 4 5 6 7 Integer count = 0 ; public void getCount () { count ++; System.out.println (count); }
很简单的一段代码,我们就来统计一下这个方法的访问次数,多个线程同时访问会不会出现什么问题,我开启的3条线程每个线程循环10次,得到一下结果
我们可以看到,这里出现了两个26,为什么会出现这种情况,出现这种情况显然表明我们这个方法根本就不是线程安全的,出现这种问题的原因有很多,我们说最常见的一种,就是我们A线程在进入方法后,拿到了count的值,刚把这个值读取出来还没有改变count的值的时候,结果线程B也进来的,那么导致线程A和线程B拿到的count值是一样的。
那么由此我们可以了解这确实不是一个线程安全的类,因为他们都需要操作这个共享的变量,其实要对线程安全问题给出一个明确的定义还是蛮复杂的,我们根据我们这个程序来总结下什么是线程安全。
当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类时线程安全的。
线程安全相关定义
在多线程程序中,i++ i+1 写入结果这些可能不是原子操作,你读我也读
volatile关键字也不能解决问题,因为它不是原子的
原子操作
原子锁,了解即可
两条线程一起执行同一个全局变量var
这个和上面那个区别并不大
C11原子类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> #include <atomic> #include <iostream> std::atomic<int > var; void *thmain (void *arg) ; int main (int argc,char *argv[]) { pthread_t thid1,thid2; if (pthread_create (&thid1,NULL ,thmain,NULL )!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); } if (pthread_create (&thid2,NULL ,thmain,NULL )!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); } printf ("join...\n" ); pthread_join (thid1,NULL ); pthread_join (thid2,NULL ); printf ("join ok.\n" ); std::cout << "var=" << var << std::endl; } void *thmain (void *arg) { for (int ii=0 ;ii<1000000 ;ii++) { var++; } }
原子操作只支持整数,效率高,但是应用场景非常有限,实际开发中,锁住对象和一串代码是无法做到的,只能用线程同步
线程同步 线程同步的三属性彻底解决了线程安全的问题。
互斥锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> int var;pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; void *thmain (void *arg) ; int main (int argc,char *argv[]) { pthread_t thid1,thid2; if (pthread_create (&thid1,NULL ,thmain,NULL )!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); } if (pthread_create (&thid2,NULL ,thmain,NULL )!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); } printf ("join...\n" ); pthread_join (thid1,NULL ); pthread_join (thid2,NULL ); printf ("join ok.\n" ); printf ("var=%d\n" ,var); pthread_mutex_destroy (&mutex); } void *thmain (void *arg) { for (int ii=0 ;ii<1000000 ;ii++) { pthread_mutex_lock (&mutex); var++; pthread_mutex_unlock (&mutex); } }
属性 了解即可
自旋锁 和互斥锁几乎一样,唯一不同的就是自旋锁它会在等待的时候不断地消耗cpu,而互斥锁不会。
但也不能说谁好谁不好,各有应用的场景。
==自旋锁==适用等待时间比较==短==的场景,而==互斥锁==适用于等待时间可能会比较==长==的场景
自旋锁没有等待超时的函数,因为他默认使用场景就是等待时间很短的
自旋锁的参数和互斥锁的区别是多了一个共享标志
这个share和private是这样的,在开发中,我们可以在进程中创建线程,线程中创建进程,但是实际开发的时候这样没有必要,毕竟程序搞得这么复杂,以后也看不懂,这个参数就是如果在多进程中创建多线程,不同进程中的线程是否能够共享锁而设计的,一般也填写priave
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> int var;pthread_spinlock_t spin; void *thmain (void *arg) ; int main (int argc,char *argv[]) { pthread_spin_init (&spin,PTHREAD_PROCESS_PRIVATE); pthread_t thid1,thid2; if (pthread_create (&thid1,NULL ,thmain,NULL )!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); } if (pthread_create (&thid2,NULL ,thmain,NULL )!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); } printf ("join...\n" ); pthread_join (thid1,NULL ); pthread_join (thid2,NULL ); printf ("join ok.\n" ); printf ("var=%d\n" ,var); pthread_spin_destroy (&spin); } void *thmain (void *arg) { for (int ii=0 ;ii<1000000 ;ii++) { pthread_spin_lock (&spin); var++; pthread_spin_unlock (&spin); } }
读写锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> #include <signal.h> pthread_rwlock_t rwlock=PTHREAD_RWLOCK_INITIALIZER; void *thmain (void *arg) ; void handle (int sig) ; int main (int argc,char *argv[]) { signal (15 ,handle); pthread_t thid1,thid2,thid3; if (pthread_create (&thid1,NULL ,thmain,NULL )!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); } sleep (1 ); if (pthread_create (&thid2,NULL ,thmain,NULL )!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); } sleep (1 ); if (pthread_create (&thid3,NULL ,thmain,NULL )!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); } pthread_join (thid1,NULL ); pthread_join (thid2,NULL ); pthread_join (thid3,NULL ); pthread_rwlock_destroy (&rwlock); } void *thmain (void *arg) { for (int ii=0 ;ii<100 ;ii++) { printf ("线程%lu开始申请读锁...\n" ,pthread_self ()); pthread_rwlock_rdlock (&rwlock); printf ("线程%lu开始申请读锁成功。\n\n" ,pthread_self ()); sleep (5 ); pthread_rwlock_unlock (&rwlock); printf ("线程%lu已释放读锁。\n\n" ,pthread_self ()); if (ii==3 ) sleep (8 ); } } void handle (int sig) { printf ("开始申请写锁...\n" ); pthread_rwlock_wrlock (&rwlock); printf ("申请写锁成功。\n\n" ); sleep (10 ); pthread_rwlock_unlock (&rwlock); printf ("写锁已释放。\n\n" ); }
条件变量 条件变量给多线程提供了复活的机制
API
在信号处理中,发送15信号,唤醒他一次(线程主函数中应该用wait使得它沉睡,等待被唤醒)
暂时先介绍到这里
信号量
API
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> #include <semaphore.h> int var;sem_t sem; void *thmain (void *arg) ; int main (int argc,char *argv[]) { sem_init (&sem,0 ,1 ); pthread_t thid1,thid2; if (pthread_create (&thid1,NULL ,thmain,NULL )!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); } if (pthread_create (&thid2,NULL ,thmain,NULL )!=0 ) { printf ("pthread_create failed.\n" ); exit (-1 ); } printf ("join...\n" ); pthread_join (thid1,NULL ); pthread_join (thid2,NULL ); printf ("join ok.\n" ); printf ("var=%d\n" ,var); sem_destroy (&sem); } void *thmain (void *arg) { for (int ii=0 ;ii<1000000 ;ii++) { sem_wait (&sem); var++; sem_post (&sem); } }
细节说明
互斥锁有两种竞争机制,1、形成等待队列 2、重新竞争
读写锁比较特别,其余几个都是形成等待队列
既然是排队,可能我们会以为是绝对公平的,但实际上并不是这样
这可能与CPU时间片或者操作系统的调度有关系,比如说当前线程虽然释放了锁,但他的时间片并没有用完,就是说本来该轮到下一个线程,但是这个线程还没有被调度,所以刚刚执行过的线程又得到了锁
那我们加一行,让cpu放弃时间片的代码,再来运行。
现在的情况好一些了
这些例子证明了等待机制没有绝对的公平,但是对应用开发没有任何影响,这里举例也只是因为怕钻牛角尖
我们有一个原则,锁的持有时间越短越好,所以实际开发中是不会出现饿死的情况。
读写锁读优先肯定有他的应用场景,如果不合适,不用就行了,不应该说这是读写锁的缺陷,而是物尽其用,都要分清场合。
linux没有提供,可以自己做一个!
生产消费者模型 概念
条件变量+互斥锁实现 我们先来搞清楚条件变量的wait做了什么
第三个步骤的两个操作是原子操作 ,只有在都成功的情况下才返回。
为何条件变量一定要跟着一把互斥锁,就是因为条件变量就是为了生产消费者模型而设计的,没有其他的用途。
细节都写在代码里了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 #include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <string.h> #include <vector> using namespace std;struct st_message { int mesgid; char message[1024 ]; }stmesg; vector<struct st_message> vcache; pthread_cond_t cond=PTHREAD_COND_INITIALIZER; pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; void incache (int sig) ; void *outcache (void *arg) ; int main () { signal (15 ,incache); pthread_t thid1,thid2,thid3; pthread_create (&thid1,NULL ,outcache,NULL ); pthread_create (&thid2,NULL ,outcache,NULL ); pthread_create (&thid3,NULL ,outcache,NULL ); pthread_join (thid1,NULL ); pthread_join (thid2,NULL ); pthread_join (thid3,NULL ); pthread_cond_destroy (&cond); pthread_mutex_destroy (&mutex); return 0 ; } void incache (int sig) { static int mesgid=1 ; struct st_message stmesg ; memset (&stmesg,0 ,sizeof (struct st_message)); pthread_mutex_lock (&mutex); stmesg.mesgid=mesgid++; vcache.push_back (stmesg); stmesg.mesgid=mesgid++; vcache.push_back (stmesg); stmesg.mesgid=mesgid++; vcache.push_back (stmesg); stmesg.mesgid=mesgid++; vcache.push_back (stmesg); pthread_mutex_unlock (&mutex); pthread_cond_broadcast (&cond); } void thcleanup (void *arg) { printf ("cleanup ok.\n" ); pthread_mutex_unlock (&mutex); }; void *outcache (void *arg) { pthread_cleanup_push (thcleanup,NULL ); struct st_message stmesg ; while (true ) { pthread_mutex_lock (&mutex); while (vcache.size ()==0 ) { pthread_cond_wait (&cond,&mutex); } memcpy (&stmesg,&vcache[0 ],sizeof (struct st_message)); vcache.erase (vcache.begin ()); pthread_mutex_unlock (&mutex); printf ("phid=%ld,mesgid=%d\n" ,pthread_self (),stmesg.mesgid); usleep (100 ); } pthread_cleanup_pop (1 ); }
信号量实现 用一个信号量代替互斥锁,一个信号量代替条件变量,信号量不存在必须解锁才能返回的情况,唯一缺点是本来一个wait搞定的现在要wait前解锁,wait后加锁(也就是从原子操作变为不是原子操作,但是没有关系),不如条件变量方便,但是在多进程的程序中只能使用信号量 ,操作系统里,也是用信号量实现,也就是条件变量+互斥锁专用于多线程。
在线程里面,生产数据只能不断用sem_post(&xxxx)
来加一信号量,想要同时生成多个就该在代码行内冗余写这句,但是在进程里就不是,只需要调用一次v操作就可以取得已经生产好的量的数值
多线程的网络服务端 这里先给出代码,很多细节都在代码里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 #include "../_public.h" CLogFile logfile; CTcpServer TcpServer; void EXIT (int sig) ; pthread_spinlock_t vthidlock; vector<pthread_t > vthid; void *thmain (void *arg) ; void thcleanup (void *arg) ; int main (int argc,char *argv[]) { if (argc!=3 ) { printf ("Using:./demo20 port logfile\nExample:./demo20 5005 /tmp/demo20.log\n\n" ); return -1 ; } CloseIOAndSignal (); signal (SIGINT,EXIT); signal (SIGTERM,EXIT); if (logfile.Open (argv[2 ],"a+" )==false ) { printf ("logfile.Open(%s) failed.\n" ,argv[2 ]); return -1 ; } if (TcpServer.InitServer (atoi (argv[1 ]))==false ) { logfile.Write ("TcpServer.InitServer(%s) failed.\n" ,argv[1 ]); return -1 ; } pthread_spin_init (&vthidlock,0 ); while (true ) { if (TcpServer.Accept ()==false ) { logfile.Write ("TcpServer.Accept() failed.\n" ); EXIT (-1 ); } logfile.Write ("客户端(%s)已连接。\n" ,TcpServer.GetIP ()); pthread_t thid; if (pthread_create (&thid,NULL ,thmain,(void *)(long )TcpServer.m_connfd)!=0 ) { logfile.Write ("pthread_create() failed.\n" ); TcpServer.CloseListen (); continue ; } pthread_spin_lock (&vthidlock); vthid.push_back (thid); pthread_spin_unlock (&vthidlock); } } void *thmain (void *arg) { pthread_cleanup_push (thcleanup,arg); int connfd=(int )(long )arg; pthread_setcanceltype (PTHREAD_CANCEL_ASYNCHRONOUS,NULL ); pthread_detach (pthread_self ()); int ibuflen; char buffer[102400 ]; while (1 ) { memset (buffer,0 ,sizeof (buffer)); if (TcpRead (connfd,buffer,&ibuflen,30 )==false ) break ; logfile.Write ("接收:%s\n" ,buffer); strcpy (buffer,"ok" ); if (TcpWrite (connfd,buffer)==false ) break ; logfile.Write ("发送:%s\n" ,buffer); } close (connfd); pthread_spin_lock (&vthidlock); for (int ii=0 ;ii<vthid.size ();ii++) { if (pthread_equal (pthread_self (),vthid[ii])) { vthid.erase (vthid.begin ()+ii); break ; } } pthread_spin_unlock (&vthidlock); pthread_cleanup_pop (1 ); } void EXIT (int sig) { signal (SIGINT,SIG_IGN); signal (SIGTERM,SIG_IGN); logfile.Write ("进程退出,sig=%d。\n" ,sig); TcpServer.CloseListen (); for (int ii=0 ;ii<vthid.size ();ii++) { pthread_cancel (vthid[ii]); } sleep (1 ); pthread_spin_destroy (&vthidlock); exit (0 ); } void thcleanup (void *arg) { close ((int )(long )arg); logfile.Write ("线程%lu退出。\n" ,pthread_self ()); }
线程安全 基于多进程网络客户端的思想,主要内容是大同小异的,但是仍然有许多细节点需要注意,第一个是退出函数,考虑因客户端网络断开而意外退出的情况,第二个是线程安全,多个客户端同时启动访问,可带来多个线程,如果不加锁的话后果不堪设想,由于我们这个服务端访问之后执行时间较短,这里采用自旋锁。另外日志文件类型也不是安全的,也需要加锁,构造析构加锁解锁,写日志时加锁,写完的时候解锁,用这个方法来简单举例:
linux大部分函数都是安全的,不安全的常见的就这几个。
在框架中用到了三个,其中localtime肯定会用到,我们框架里用到的是不安全版本的,注释里的就是安全版本的
拓展学习 看看就好,了解一下不是坏事
面试题 能做出这道题,基本上可以算是合格的程序员。
保证服务程序稳定性 多线程来做有一个好处,监控,调度,程序的功能在同一个程序中,我们之前那个使用了三个程序。
异步通讯
十三、数据服务总线
直连数据访问速度快,并且使用方便,通过数据服务总线(接口),则需要按照HTTP条条框框执行,并且不是随心所欲,这两种方式都有对应的应用场景
HTTP协议的本质 通信方式 HTTP协议的通信方式是最简单直接的,客户端发起TCP连接请求,连接成功后发起请求报文,服务端响应,采用短连接的话通信一次即断开,长连接可以多次通信。
报文格式 把请求的报文按这个格式拼接成一个字符串,发送给服务端就行了。
另外http是不安全的链接,https是安全的,在浏览器会显示一把锁。
GET / HTTP/1.1
是请求行
这个例子,后面的都是请求头部,在GET方法里没有请求数据,别的方法里有些有,有些没有。
头部字段一般比较多,必填的是Host,Connection和Port比较有意义
客户端判断响应是否结束 Content-Length可以不写,写了的话,如果长度多了或者少了,无法显示内容
wget +iconv wget+地址即可拉取下来。支持http和https,我们这个demo只支持http
iconv可以将一个文件的字符集从一种格式转化为另外一种
数据服务总线概念 给HTTP的数据访问接口起一个高大上的名字,数据访问总线就是这么来的。
简单demo实现 所要实现的框架大体是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 CTcpServer TcpServer; if (TcpServer.InitServer (atoi (argv[1 ]))==false ) { printf ("TcpServer.InitServer(%s) failed.\n" ,argv[1 ]); return -1 ; } if (TcpServer.Accept ()==false ) { printf ("TcpServer.Accept() failed.\n" ); return -1 ; } printf ("客户端(%s)已连接。\n" ,TcpServer.GetIP ()); char strget[102400 ]; memset (strget,0 ,sizeof (strget)); recv (TcpServer.m_connfd,strget,1000 ,0 ); printf ("%s\n" ,strget); char strsend[102400 ]; memset (strsend,0 ,sizeof (strsend)); sprintf (strsend,\ "HTTP/1.1 200 OK\r\n" \ "Server: demo28\r\n" \ "Content-Type: text/html;charset=utf-8\r\n" \ "\r\n" ); if (Writen (TcpServer.m_connfd,strsend,strlen (strsend))== false ) return -1 ; SendData (TcpServer.m_connfd,strget); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 bool getvalue (const char *strget,const char *name,char *value,const int len) { value[0 ]=0 ; char *start,*end; start=end=0 ; start=strstr ((char *)strget,(char *)name); if (start==0 ) return false ; end=strstr (start,"&" ); if (end==0 ) end=strstr (start," " ); if (end==0 ) return false ; int ilen=end-(start+strlen (name)+1 ); if (ilen>len) ilen=len; strncpy (value,start+strlen (name)+1 ,ilen); value[ilen]=0 ; return true ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 bool SendData (const int sockfd,const char *strget) { char username[31 ],passwd[31 ],intername[30 ],obtid[11 ],begintime[21 ],endtime[21 ]; memset (username,0 ,sizeof (username)); getvalue (strget,"username" ,username,30 ); ............. printf ("username=%s\n" ,username); ............. connection conn; conn.connecttodb ("scott/tiger@snorcl11g_132" ,"Simplified Chinese_China.AL32UTF8" ); sqlstatement stmt (&conn) ; stmt.prepare ("select '<obtid>'||obtid||'</obtid>'||'<ddatetime>'||to_char(ddatetime,'yyyy-mm-dd hh24:mi:ss')||'</ddatetime>'||'<t>'||t||'</t>'||'<p>'||p||'</p>'||'<u>'||u||'</u>'||'<keyid>'||keyid||'</keyid>'||'<endl/>' from T_ZHOBTMIND1 where obtid=:1 and ddatetime>to_date(:2,'yyyymmddhh24miss') and ddatetime<to_date(:3,'yyyymmddhh24miss')" ); char strxml[1001 ]; stmt.bindout (1 ,strxml,1000 ); stmt.bindin (1 ,obtid,10 ); stmt.bindin (2 ,begintime,14 ); stmt.bindin (3 ,endtime,14 ); stmt.execute (); Writen (sockfd,"<data>\n" ,strlen ("<data>\n" )); while (true ) { memset (strxml,0 ,sizeof (strxml)); if (stmt.next ()!=0 ) break ; strcat (strxml,"\n" ); Writen (sockfd,strxml,strlen (strxml)); } Writen (sockfd,"</data>\n" ,strlen ("</data>\n" )); return true ; }
url小提示 url中不会出现空格,在语句中出现的空格,会在对方端显示为%20
一个URL的基本组成部分包括协议(scheme),域名,端口号,路径和查询字符串(路径参数和锚点标记就暂不考虑了)。路径和查询字符串之间用问号?
分离。例如http://www.example.com/index?param=1,路径为index,查询字符串 (Query String)为param=1。URL中关于空格的编码正是与空格所在位置相关:空格被编码成加号+的情况只会在查询字符串部分出现,而被编码成%20则可以出现在路径和查询字符串中。
功能需求
表的设计 一些注意点,比如数据种类的定义那张表,外键指向自己,这种设计方式一般适用于某种具有层次关系的表
有层次的ID,取值技巧:比如说第一种类,用01,02,03,04这些来表示,第二层次就分别先带上01的ID,再后面延续,并且排序
每连接每线程实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 void *thmain (void *arg) { pthread_cleanup_push (thcleanup,arg); int connfd=(int )(long )arg; pthread_setcanceltype (PTHREAD_CANCEL_ASYNCHRONOUS,NULL ); pthread_detach (pthread_self ()); char strrecvbuf[1024 ]; memset (strrecvbuf,0 ,sizeof (strrecvbuf)); if (ReadT (connfd,strrecvbuf,sizeof (strrecvbuf),3 )<=0 ) pthread_exit (0 ); if (strncmp (strrecvbuf,"GET" ,3 )!=0 ) pthread_exit (0 ); logfile.Write ("%s\n" ,strrecvbuf); connection conn; if (conn.connecttodb (starg.connstr,starg.charset)!=0 ) { logfile.Write ("connect database(%s) failed.\n%s\n" ,starg.connstr,conn.m_cda.message); pthread_exit (0 ); } if (Login (&conn,strrecvbuf,connfd)==false ) pthread_exit (0 ); if (CheckPerm (&conn,strrecvbuf,connfd)==false ) pthread_exit (0 ); char strsendbuf[1024 ]; memset (strsendbuf,0 ,sizeof (strsendbuf)); sprintf (strsendbuf,\ "HTTP/1.1 200 OK\r\n" \ "Server: webserver\r\n" \ "Content-Type: text/html;charset=utf-8\r\n\r\n" ); Writen (connfd,strsendbuf,strlen (strsendbuf)); if (ExecSQL (&conn,strrecvbuf,connfd)==false ) pthread_exit (0 ); pthread_cleanup_pop (1 ); }
Login() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 bool Login (connection *conn,const char *buffer,const int sockfd) { char username[31 ],passwd[31 ]; getvalue (buffer,"username" ,username,30 ); getvalue (buffer,"passwd" ,passwd,30 ); sqlstatement stmt; stmt.connect (conn); stmt.prepare ("select count(*) from T_USERINFO where username=:1 and passwd=:2 and rsts=1" ); stmt.bindin (1 ,username,30 ); stmt.bindin (2 ,passwd,30 ); int icount=0 ; stmt.bindout (1 ,&icount); stmt.execute (); stmt.next (); if (icount==0 ) { char strbuffer[256 ]; memset (strbuffer,0 ,sizeof (strbuffer)); sprintf (strbuffer,\ "HTTP/1.1 200 OK\r\n" \ "Server: webserver\r\n" \ "Content-Type: text/html;charset=utf-8\r\n\r\n" \ "<retcode>-1</retcode><message>username or passwd is invailed</message>" ); Writen (sockfd,strbuffer,strlen (strbuffer)); return false ; }
CheckPerm() 真的就这点点不同=-=,和Login()
1 2 3 4 5 6 stmt.prepare ("select count(*) from T_USERANDINTER where username=:1 and intername=:2 and intername in (select intername from T_INTERCFG where rsts=1)" ); sprintf (strbuffer,\ ....... "<retcode>-1</retcode><message>permission denied</message>" );
ExecSQL()
我们一段一段描述:
getvalue解析接口名
申明几个变量,用sqlstatement stmt对象将参数拿出来(接口SQL,输出列名,接口参数)
prepare()准备SQL语句
绑定输入输出变量。
execute()执行,使得SQL语句good,得到应该得到的东西
再次用getvalue,将输从url中获取的参数绑定到对应的变量中
发送标签是用Writen()函数
最关键的那一部分是这样做到的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 char strtemp[2001 ]; while (true ){ memset (strsendbuffer,0 ,sizeof (strsendbuffer)); memset (colvalue,0 ,sizeof (colvalue)); if (stmt.next () != 0 ) break ; for (int ii=0 ;ii<CmdStr.CmdCount ();ii++) { memset (strtemp,0 ,sizeof (strtemp)); snprintf (strtemp,2000 ,"<%s>%s</%s>" ,CmdStr.m_vCmdStr[ii].c_str (),colvalue[ii],CmdStr.m_vCmdStr[ii].c_str ()); strcat (strsendbuffer,strtemp); } strcat (strsendbuffer,"<endl/>\n" ); Writen (sockfd,strsendbuffer,strlen (strsendbuffer)); }
缺点
优化方案
数据库连接池
数据库连接池和公共卫生局的原理和算法是一样的。
声明定义 有两个注意点需要重视,数据库连接池的数组有多大,就需要声明多少把锁,而不是共用一把锁,只能用互斥锁,不能用自旋锁,因为如果数据量比较大,数据库连接占用的时间可能比较长,自旋锁不断刷新,不合适。
1 2 3 4 5 6 7 #define MAXCONNS 10 connection conns[MAXCONNS]; pthread_mutex_t mutex[MAXCONNS]; bool initconns () ; connection *getconns () ; bool freeconns (connection *conn) ; bool destroyconns () ;
initconns() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 bool initconns () { for (int ii=0 ;ii<MAXCONNS;ii++) { if (conns[ii].connecttodb (starg.connstr,starg.charset) != 0 ) { logfile.Write ("connect database(%s) failed.\n%s\n" ,starg.connstr,conns[ii].m_cda.message); return false ; } pthread_mutex_init (&mutex[ii],0 ); } return true ; }
getconns() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 connection *getconns () { while (true ) { for (int ii=0 ;ii<MAXCONNS;ii++) { if (pthread_mutex_trylock (&mutex[ii])==0 ) { logfile.Write ("get conns is %d.\n" ,ii); return &conns[ii]; } } usleep (10000 ); } }
freeconns(connection *conn) 1 2 3 4 5 6 7 8 9 10 11 12 bool freeconns (connection *conn) { for (int ii=0 ;ii<MAXCONNS;ii++) { if (&conns[ii]==conn) { pthread_mutex_unlock (&mutex[ii]); return true ; } } return false ; }
destroyconns() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 bool destroyconns () { for (int ii=0 ;ii<MAXCONNS;ii++) { conns[ii].disconnect (); pthread_mutex_destroy (&mutex[ii]); } return true ; }
连接池性能
优化 最开始我们是人为的设定线程池的大小,现在我们需要对他进行优化,不然太生硬了
因此定义一个专门的线程池类,方便操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class connpool { private : struct st_conn { connection conn; pthread_mutex_t mutex; time_t atime; }*m_conns; int m_maxconns; int m_timeout; char m_connstr[101 ]; char m_charset[101 ]; public : connpool (); ~connpool (); bool init (const char *connstr,const char *charset,int maxconns,int timeout) ; void destroy () ; connection *get () ; bool free (connection *conn) ; void checkpool () ; };
connpool::get() 其实就是前面getconns()demo 的一个更完善的实现。
这串代码先用trylock找出了第一个空闲的位置,如果存在这样一个位置,则将他赋值给pos,并后续将其连接上,如果未找到,则将这次的锁给释放掉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 connection *connpool::get () { int pos=-1 ; for (int ii=0 ;ii<m_maxconns;ii++) { if (pthread_mutex_trylock (&m_conns[ii].mutex)==0 ) { if (m_conns[ii].atime>0 ) { printf ("取到连接%d。\n" ,ii); m_conns[ii].atime=time (0 ); return &m_conns[ii].conn; } if (pos==-1 ) pos=ii; else pthread_mutex_unlock (&m_conns[ii].mutex); } } if (pos==-1 ) { printf ("连接池已用完。\n" ); return NULL ; } printf ("新连接%d。\n" ,pos); if (m_conns[pos].conn.connecttodb (m_connstr,m_charset)!=0 ) { printf ("连接数据库失败。\n" ); pthread_mutex_unlock (&m_conns[pos].mutex); return NULL ; } m_conns[pos].atime=time (0 ); return &m_conns[pos].conn; }
使用连接池类后的线程主函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 void *thmain (void *arg) { pthread_cleanup_push (thcleanup,arg); int connfd=(int )(long )arg; pthread_setcanceltype (PTHREAD_CANCEL_ASYNCHRONOUS,NULL ); pthread_detach (pthread_self ()); char strrecvbuf[1024 ]; memset (strrecvbuf,0 ,sizeof (strrecvbuf)); if (ReadT (connfd,strrecvbuf,sizeof (strrecvbuf),3 )<=0 ) pthread_exit (0 ); if (strncmp (strrecvbuf,"GET" ,3 )!=0 ) pthread_exit (0 ); logfile.Write ("%s\n" ,strrecvbuf); connection *conn=oraconnpool.get (); char strsendbuf[1024 ]; if (conn==0 ) { usleep (100000 ); memset (strsendbuf,0 ,sizeof (strsendbuf)); sprintf (strsendbuf,\ "HTTP/1.1 200 OK\r\n" \ "Server: webserver\r\n" \ "Content-Type: text/html;charset=utf-8\r\n\r\n" \ "<retcode>-1</retcode><message>internal error.</message>" ); Writen (connfd,strsendbuf,strlen (strsendbuf)); pthread_exit (0 ); } if (Login (conn,strrecvbuf,connfd)==false ) { oraconnpool.free (conn); pthread_exit (0 ); } if (CheckPerm (conn,strrecvbuf,connfd)==false ) { oraconnpool.free (conn); pthread_exit (0 ); } memset (strsendbuf,0 ,sizeof (strsendbuf)); sprintf (strsendbuf,\ "HTTP/1.1 200 OK\r\n" \ "Server: webserver\r\n" \ "Content-Type: text/html;charset=utf-8\r\n\r\n" ); Writen (connfd,strsendbuf,strlen (strsendbuf)); if (ExecSQL (conn,strrecvbuf,connfd)==false ) { oraconnpool.free (conn); pthread_exit (0 ); } oraconnpool.free (conn);; pthread_cleanup_pop (1 ); }
容器中还有两个进程未退出,具体解决方法看上面代码块的usleep注释
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void thcleanup (void *arg) { close ((int )(long )arg); pthread_spin_lock (&vthidlock); for (int ii=0 ;ii<vthid.size ();ii++) { if (pthread_equal (pthread_self (),vthid[ii])) { vthid.erase (vthid.begin ()+ii); break ; } } pthread_spin_unlock (&vthidlock); logfile.Write ("线程%lu退出。\n" ,pthread_self ()); }
线程池 和连接池的思路不一样,并且同样情况下性能比连接池提升了四五倍
生产消费者模型条件变量+互斥锁,有客户端连接,就是生产了一个产品,再在thmain中处理这个任务,类似于消费这个产品。工作主函数就是消费者模型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 if (oraconnpool.init (starg.connstr,starg.charset,10 ,50 )==false ){ logfile.Write ("oraconnpool.init() failed.\n" ); return -1 ; } else { pthread_t thid; if (pthread_create (&thid,NULL ,checkpool,0 )!=0 ) { logfile.Write ("pthread_create() failed.\n" ); return -1 ; } } for (int ii=0 ;ii<10 ;ii++){ pthread_t thid; if (pthread_create (&thid,NULL ,thmain,(void *)(long )ii)!=0 ) { logfile.Write ("pthread_create() failed.\n" ); return -1 ; } vthid.push_back (thid); } pthread_spin_init (&spin,0 ); while (true ){ if (TcpServer.Accept ()==false ) { logfile.Write ("TcpServer.Accept() failed.\n" ); return -1 ; } logfile.Write ("客户端(%s)已连接。\n" ,TcpServer.GetIP ()); pthread_mutex_lock (&mutex); sockqueue.push_back (TcpServer.m_connfd); pthread_mutex_unlock (&mutex); pthread_cond_signal (&cond); }
线程池的监控
在整个程序中,需要等待的地方只有wait哪里,也就是我们工作进程的心跳信息更新地点
1 2 3 4 5 6 while (sockqueue.size ()==0 ) { pthread_cond_wait (&cond,&mutex); } ------------------------->
1 2 3 4 5 6 7 8 9 10 11 12 13 14 while (true ){ pthread_mutex_lock (&mutex); while (sockqueue.size ()==0 ) { struct timeval now ; gettimeofday (&now,NULL ); now.tv_sec=now.tv_sec+20 ; pthread_cond_timedwait (&cond,&mutex,(struct timespec*)&now); vthid[pthnum].atime=time (0 ); }
监控线程 并放置主函数中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void *checkthmain (void *arg) { while (true ) { for (int ii=0 ;ii<vthid.size ();ii++) { if ((time (0 )-vthid[ii].atime)>25 ) { logfile.Write ("thread %d(%lu) timeout(%d).\n" ,ii,vthid[ii].pthid,time (0 )-vthid[ii].atime); pthread_cancel (vthid[ii].pthid); if (pthread_create (&vthid[ii].pthid,NULL ,thmain,(void *)(long )ii)!=0 ) { logfile.Write ("pthread_create() failed.\n" ); EXIT (-1 ); } vthid[ii].atime=time (0 ); } } sleep (3 ); } }
数据安全策略
通常由硬件解决,软件解决反而麻烦很多,通常不需要我们考虑。
我们能做些什么?
软件意义的登录,和唯一识别。
黑名单和白名单逻辑上是冲突的,只能二选一,不能同时要
前面两种是系统层面的,不针对具体用户,第三种是针对具体用户的。
思路:
不管采用那种,都需要修改这个数据结构:
可以选择一个结构体,除了采用客户端的socket,也要采用客户端的ip地址
如果采用绑定ip的方法,修改Login代码就可以了。
拿出IP地址,与客户端连接上的对比即可。
为何要将黑名单和白名单放在容器中呢?,原因很简单,这种名单一般是量很少,每当有用户登录,查找内存要比查找数据库快得多 。对于数据库操作,要有一个原则,能够操作数据库,就不操作数据库,这会很快。
并且,判断是否为黑名单白名单的代码也要放在工作线程中,不要直接在socket刚刚连接加锁就解决这个问题,原子操作尽量保证内容少。
学习总结
让每个数据库的数据都是一样的,反正MySQL不要钱,不过使用过程中有一个细节,需要均匀分摊,第一个用户连了A库,那第二个就连B,总之要将用户量平摊,使得每个数据库分配的量均匀。
其实本质都是一样的,为了传输,只是各自的约定不同,带来的效率,安全性,稳定性等因素也会不同。
十四、IO复用&网络代理
三种模型
正向代理&反向代理
select模型 先来了解了解TCP缓冲区
(18条消息) tcp缓冲区_jigetage的博客-CSDN博客_tcp缓冲区
什么是tcp缓冲区?每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
二、缓冲区的意义 write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。 TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,比如nagle算法,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。 read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。
如果TCP连接断开,网络缓冲区还有为发出去的数据,下次连接后会继续发送么,还是清空缓冲区?
接收端tcp断开,分为两种情况:1. 调用close主动断开,执行tcp三次挥手,正常结束。2. 接收端断网或者宕机,此时发送端是无法知道接收端已经断开,会一直往那个已经断开的socket缓冲区发数据,知道缓冲区满就会阻塞;如果接收端重新连接上来的话,此时不会继续发送数据,因为重连的socket已经变了,缓冲区会进行了重建;老的已经断开的socket需要发送端自己处理,即调用 clientSocket.close() 清理掉废弃的socket回收相应资源
这是select的帮助文档,readfds是需要监视可读参数的集合,writefds反之,timeout是超时时间。第一个nfds下面讲
fd_set我们可能看不懂是啥,可以去看看他的声明。
把它替换一下。
这张图的意思是,假设现在有一个socket的集合,他们的socketID分别是3,4,5,6,就把位图中对应位置置为1就可以了,现在有一个新的socket连接上来,它的ID取值是9,那么就把第9个位置置为1即可!
如果有连接退出,将其置为0即可。
Linux提供的四个宏,用于操作位图,
CLR:将某一个socket置为空,ISSET:判断某一个socket在集合中,SET:将某一个socket置为1,ZERO:将全局置为空。
现在我们可以了解nfds的意义了,它是指readfds,writefds,exceptfds三个位图中,最大的那个,也就是整个图有多大,exceptfds填NULL就行了,后面发展的模型都没用他了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 int listensock = initserver (atoi (argv[1 ]));printf ("listensock=%d\n" ,listensock);if (listensock < 0 ) { printf ("initserver() failed.\n" ); return -1 ; }fd_set readfds; FD_ZERO (&readfds); FD_SET (listensock,&readfds); int maxfd=listensock; while (true ){ fd_set tmpfds=readfds; struct timeval timeout ; timeout.tv_sec=10 ; timeout.tv_usec=0 ; int infds=select (maxfd+1 ,&tmpfds,NULL ,NULL ,&timeout); if (infds < 0 ) { perror ("select() failed" ); break ; } if (infds == 0 ) { printf ("select() timeout.\n" ); continue ; } for (int eventfd=0 ;eventfd<=maxfd;eventfd++) { if (FD_ISSET (eventfd,&tmpfds)<=0 ) continue ; if (eventfd==listensock) { struct sockaddr_in client ; socklen_t len = sizeof (client); int clientsock = accept (listensock,(struct sockaddr*)&client,&len); if (clientsock < 0 ) { perror ("accept() failed" ); continue ; } printf ("accept client(socket=%d) ok.\n" ,clientsock); FD_SET (clientsock,&readfds); if (maxfd<clientsock) maxfd=clientsock; } else { char buffer[1024 ]; memset (buffer,0 ,sizeof (buffer)); if (recv (eventfd,buffer,sizeof (buffer),0 )<=0 ) { printf ("client(eventfd=%d) disconnected.\n" ,eventfd); close (eventfd); FD_CLR (eventfd,&readfds); if (eventfd == maxfd) { for (int ii=maxfd;ii>0 ;ii--) { if (FD_ISSET (ii,&readfds)) { maxfd = ii; break ; } } } } else { printf ("recv(eventfd=%d):%s\n" ,eventfd,buffer); fd_set tmpfds; FD_ZERO (&tmpfds); FD_SET (eventfd,&tmpfds); if (select (eventfd+1 ,NULL ,&tmpfds,NULL ,NULL )<=0 ) perror ("select() failed" ); else send (eventfd,buffer,strlen (buffer),0 ); } } } }
由于时间关系,剩下的select和poll,后面再聊了
epoll模型
创造句柄,类似于select的bit map,注册事件,意思是告诉socket需要监视那些事件,接下来调用wait等待事件的发生。
十五、课程总结 非结构化数据存储方案 总的来说,没有最好的方案,只有最合适的方案。
https://blog.csdn.net/PIPJIN961111/article/details/102664666
结构化与半结构化与非结构化数据 1.结构化数据 结构化的数据是指可以使用关系型数据库表示存储,表现为二维形式的数据。一般特点是:数据以行为单位,一行数据表示一个实体的信息,每一行数据的属性是相同的。例子:
1 2 3 4 id name age gender 1 lyh 12 male 2 liangyh 13 female 3 liang 18 male
所以,结构化的数据的存储和排列是很有规律的,这对查询和修改等操作很有帮助
但是它的扩展性不好,比如需要的时候加个字段,在实际运用中每次都进行反复的表结构变更,这容易导致后台接口从数据库取数据出错。
2.半结构化数据
半结构是有点勉强的说法,可以当做结构化,也可以当做非结构化,这里我们当做非结构化。
半结构化数据是结构化数据的一种形式,它并不符合关系型数据库或其他数据表的形式关联起来的数据模型结构,但包含相关标记,用来分隔语义元素以及对记录和字段进行分层。因此,它也被称为自描述的结构。
半结构化数据,属于同一类实体可以有不同的属性,即使他们被组合在一起,这些属性的顺序并不重要。
常见的半结构数据有XML和JSON,对于XML文件,例如
1 2 3 4 5 <person > <name > A</name > <age > 13</age > <gender > female</gender > </person >
从上面的例子中,属性的顺序是不重要的,不同的半结构化数据的属性的个数是不一定一样的。
标签是树的根节点,和`标签是子节点。通过这样的数据格式,可以自由地表达很多有用的信息,包括自我描述信息(元数据)。所以,半结构化数据的扩展性是很好的。
3.非结构化数据
非结构化数据是数据结构不规则或不完整,没有预定义的数据模型,不方便用数据库二维逻辑表来表现的数据。包括所有格式的办公文档、文本、图片、各类报表、图像和音频/视频信息等等。
hbase数据库是一个NoSql(Not Only SQL,泛指非关系型数据库)。
Hbase是一个分布式的、面向列,运行在HDFS上的数据库
适合存储访问超大规模的数据集,可以提供数据的实时随机读写
*方案一
*意味着用的多
方便简洁,我们还有文件同步程序,快速实现同步功能。
*方案二 如果非结构化数据很少,也可以用啊
数据量大其实也可以用,具体要采用BOLB缓存技术
BLOB缓存
方案三 如果软件有价值,这种方案肯定最合适
*方案四
方案五 至少90%的企业和政府是没有大数据的,用不上这个东西
总的来说HDFS是个好东西(分布式文件系统),适用于大数据,普通的小项目没必要用。
方案六 哪些所谓的云存储,都是建立在别人的基础上搭建的虚拟平台而已,没什么好特别的。
数据中心辅助模块 通常没什么人用,就自己用。
一个朴素的页面,拓展知识就好。
实时同步 早期采用。
服务器资源信息获取
数据表的设计技巧
J:\11Projectc++\课程文档(1)\oracle数据库\37.索引的本质与SQL优化.docx
例如我们在这里产生了外键,其实可以采取移除外键,并用更多相同的变量名来定位,外键保证了数据的完整性,但是也增加了数据库的开销,另外从观测数据表中读取数据的时候往往也需要全国站点参数中把站点名称,经纬度,海拔高度取出来。可以使用关联查询,但是这种方式也肯定比单表查询要慢。
可以设计成这样,这样会浪费磁盘空间,但现在的社会磁盘空间已经不值钱了。另外也不能保证数据的完整性,但是只要应用程序不乱搞通常也无大碍。
最好delete都不要,只insert
解决问题的办法就是,将更新一个表的操作,转换为insert一个表的操作(多个用户操作一个表,必然update存在竞争)
当遇到七天过后还没签收之类的客户,肯定不能用常规的操作处理了
我们可以考虑把它的信息做成归档表,其中,物流信息用xml或者json等格式来存放就好
触发器、自定义函数和存储过程 数据库是项目的瓶颈,能不让他去做,就不让他去做。另外它的编程语言也比较菜=-=
触发器也是以前用的多,只要框架里有的,都可以不用,下面这就是用于兼容问题的代码。
数据的缓存方案 数据为什么要缓存?
还是因为数据库太慢,Redis和Memcache做缓存已经不是秘密。
传统数据库稳,但是不快。
但是有的时候Redis和Memcache也不能很好得装下大量的数据,这时候,文件缓存不失为一种很好的解决办法。
第一级是年月(记录存放一个月)第二级是每个城市的号码段,第三极目录是号码的数字,每个txt存放了这一个月的详单文件。详单内容按时间排序,再写一个TCP程序提供详单查询,采用HTTP协议,就像数据服务总线一样。
项目经验 求职面试过程
一些技巧
项目介绍实例
获取项目背景资源 我们如何打造这样一个介绍呢?
从这些地方找
百度 这些是能在网上找到的项目截图。
也要记得去门户网站和官网看
招标平台 搜索关键字::
数据开放平台
另外,这个项目和我们课程中的项目实在是太像了,课程开发的不需要任何修改,都能满足80%以上的需求。
其他细节 后台,三四个,前端,三四个啥的,打错了没关系,但不能吞吞吐吐。
优化工作就是指:比如以前对每一种程序编写一个入库程序,后来,我采用读取数据字典,采用XML这种方法,做成了通用的功能。
另外,负责通用模块开发,对具体业务不太熟,可以推掉很多问题,不知道有多少种数据,有多少数据量都可以理解,但是技术的细节,一定要清楚。
如果还不够,我们还可以新增一个APP项目
我们把APP下载下来,每个功能都用一下,非常简单。APP软件功能一般都非常简单,采用HTTP或者自定义TCP协议都可以,
另外想好面试官可能的提问,例如:
课程总结