环境搭建
1.安装vim
sudo apt-get install vim-gtk
2.配置ip,网关,子网掩码
sudo vim /etc/network/interfaces
3.配置DNS
sudo vim /etc/resolvconf/resolv.conf.d/base
输入 nameserver 8.8.8.8
保存并退出
(要重启)
PS:可以在配置网卡的时候同时配置DNS,如上图中可以在最后插入一句:
dns-nameserver 8.8.8.8
附加:关于/etc/resolv.conf重启失效问题(资料来源于网络)
最近使用了最新版的ubuntu 18.04运行一些服务,然后发现服务器经常出现网络不通的情况,主要是一些域名无法解析。
检查/etc/resolv.conf,发现之前修改的nameserver总是会被修改为127.0.0.53,无论是改成啥,过段时间,总会变回来。
查看/etc/resolv.conf这个文件的注释,发现开头就写着这么一行:
This file is managed by man:systemd-resolved(8). Do not edit.
这说明这个文件是被systemd-resolved这个服务托管的。
通过netstat -tnpl| grep systemd-resolved查看到这个服务是监听在53号端口上。
查了下,这个服务的配置文件为/etc/systemd/resolved.conf,大致内容如下:
[Resolve]
DNS=1.1.1.1 1.0.0.1
#FallbackDNS=
#Domains=
LLMNR=no
#MulticastDNS=no
#DNSSEC=no
#Cache=yes
#DNSStubListener=yes
如果我们要想让/etc/resolve.conf文件里的配置生效,需要添加到systemd-resolved的这个配置文件里DNS配置项(如上面的示例,已经完成修改),然后重启systemd-resolved服务即可。
另一种更简单的办法是,我们直接停掉systemd-resolved服务,这样再修改/etc/resolve.conf就可以一直生效了。
4.安装ssh远程连接服务
sudo apt-get install openssh-server
5.安装gcc,g++编译器
sudo apt-get install build-essential
6.在windows下创建一个共享目录
(1)在vmware相应的虚拟机上,右键,点设置
(2)在Linux下查看
共享的文件夹在 /mnt/hgfs 下
nginx安装
1.安装nginx前提
(1)Linux内核在2.6或者以上,因为epoll技术的依赖
uname -a 来查看内核版本
(2)gcc,g++编译器
sudo apt-get install build-essential //前面已安装好
(3)pcre库:一个函数库,支持解析正则表达式
sudo apt-get install libpcre3-dev
(4)zlib库:支持压缩解压功能
sudo apt-get install libz-dev
(5)openssl库:ssl相关功能库,用于网站加密通讯(不一定要安装)
sudo apt-get install libssl-dev
2.下载nginx
(1)www.nginx.org
(2)点右边的download
(3)下载stable版本
(4)wget http://nginx.org/download/nginx-1.16.1.tar.gz
(5)tar -zxvf nginx-1.16.1.tar.gz
3.nginx目录结构
4.安装nginx
(1)进入解压后的目录
(2)执行./configure //做编译安装前的一些配置
(3)make
(4)sudo make install //默认安装到/usr/local下
5.运行nginx
(1)进入安装目录/usr/local/
(2)进入nginx/sbin
(3)执行sudo ./nginx
(4)nginx默认监听80端口
6.检查
(1)浏览器进入地址 http://192.168.1.66 //Ubuntu服务器主机的ip地址
nginx整体结构,进程模型
一、特点
1.一个master进程对应多个worker进程
2.master进程只负责监控和管理worker进程,而处理业务的事情由worker进程来完成,所以,这种模型保证了nginx的稳定,灵活
3.master进程和worker进程之间也要进行通信,可以用信号或者共享内存来通信
4.worker进程一旦挂掉,master进程会立即用fork()函数来创建一个新的worker进程投入到工作中
二、worker进程数量
1.worker进程的数量为多少才合适?
答:公认的做法:多核计算机,就让每个worker进程运行在一个单独的内核上,最大限度减少CPU进程切换成本,提高系统效率
2.查看当前Ubuntu的内核数量:
grep -c processor /proc/cpuinfo
3.物理主机的内核数是8,在vmware改Ubuntu系统的处理器内核数量
4.改worker进程数量,让它等于内核数,达到最大效率:
进入nginx目录,进入conf文件夹,sudo vi nginx.conf,
5.检查worker进程数量:
sudo ./nginx
ps -ef | grep nginx
1.通过sudo ./nginx -?来查看nginx所支持的选项
3.nginx重载配置文件
当修改好了conf文件夹下的nginx.conf文件后,可以通过进入sbin目录下,然后sudo ./nginx -s reload来重载nginx的配置文件
3.nginx的关闭
(1)根据sudo ./nginx -?得出,在-s选项中,有stop信号和quit信号,stop方式是比较粗暴的直接退出,而quit方式是先处理完当前的任务再退出
(2)用法:sudo ./nginx -s stop或者sudo ./nginx -s quit
学习nginx源码的准备工作
1.解决在vmware中开启了文件夹共享后,在Ubuntu中看不到文件夹的问题
(1)原因:一般情况下,可能是vmware tools工具出了什么问题
(2)解决方法:重装vmware tools工具
(3)步骤:
(a)
(b)进入Ubuntu中,进入/mnt
创建个文件夹
sudo mkdir cdrom
挂载磁盘到该文件夹
sudo mount /dev/cdrom /mnt/cdrom
然后进入/mnt/cdrom,即可看到vmware tools的安装包,然后tar -zxvf 安装包名 进行解压,然后
安装完成即可解决问题
nginx源码学法,终端和进程的关系说
1.Linux进程组,会话关系图
(1)会话包含一个或多个进程组,一个进程组包含一个或多个进程
(2)可以调用系统函数来加入、创建进程组
(3)一般来说,只要不进行特殊的系统函数调用,那么一个bash(shell)上边运行的所有程序都属于一个会话,而这个会话会有一个session leader,那么这个bash(shell)通常就是session leader
(4)ps -eo pid,ppid,sid,tty,comm,stat | grep -E “bash|nginx”
2.(1)把虚拟终端(如XShell或SecureCRT等)关掉时,系统会发送SIGHUP信号给session leader,那就是bash进程。
(2)bash进程收到SIGHUP信号后,bash会把这个信号发送给session里的所有进程,收到这个SIGHUP信号的进程的缺省动作一般来说就是退出
3.strace工具
(1)Linux下调试分析诊断工具,可跟踪程序执行时进程的调用以及所收到的信号
(2)用法:strace -e trace=signal -p 进程id。-e选项的作用是让strace工具附着在要跟踪的进程上
(3)例子:跟踪自己写的nginx程序进程
给nginx进程发一个暂停信号:
kill -19 1438
结果:
4.终端关闭时如何让进程不退出
(1)方法一:在nginx程序代码中编写相关代码来拦截(忽略)SIGHUP信号
如上图,一开始nginx的ppid为1295,关掉运行nginx的终端(即给nginx发送一个SIGHUP信号)
nginx的父进程id变为1,即nginx没有退出,还在运行
(2)方法二:让nginx进程和它所在的终端的bash进程不在同一个session里
(a)可以通过调用setsid()函数来实现
(b)如:
(c)解析
1)在pid = fork();执行完时,子进程已创建,那么子进程和父进程在该处同时往下执行下面的代码
2)setsid()函数在子进程中调用才起作用,在进程组组长所在的进程里调用会无效
3)这种方法,在关闭终端后,父进程退出,而子进程不会退出,仍继续执行
(3)方法三:用setid命令
(a)作用:启动一个进程,且该进程在一个新的session中,这样终端关闭时该进程就不会退出
(b)用法:setid ./nginx
(4)方法四:用nohup命令
(a)道理跟方法一一样
(b)用法:nohup ./nginx
(c)特点:在屏幕上看不到输出,输出重定向到了当前目录下的一个文件:nohup.out
(5)方法五:后台运行(不可行)
(a)用法:在进程后加&,如 ./nginx &
(b)切换回前台的命令:fg
(c)这个方法,关闭终端后进程也会退出,所以不可行
信号的概念,认识,处理动作
1.每个信号其实是一个宏,即一个整型值
(1)查看信号:
sudo find / -name “signal.h” | xargs grep -in “SIGHUP”
结果中最后有个:
然后:
2.直接kill一个进程,如kill 6,那其实是给进程id为6的这一进程发送SIGTERM信号。
(1)如果在那个进程的程序代码中没有写关于处理SIGTERM信号的代码,那么缺省动作一般是退出进程
(2)通过kill -数字 pid 来给pid发送对应信号
3.查看进程状态
(1)ps -eo pid,ppid,sid,pgrp,comm,stat | grep -E “bash|nginx”
(2)ps -aux | grep -E “bash|nginx” //BSD风格的显示格式
4.SIGKILL和SIGSTOP信号是不能被忽略的,即不能用代码来忽略它们,即使用写了忽略它们的相关代码,那么它们本身的缺省动作依旧会执行
5.信号处理的动作分类
(1)执行系统的默认动作:没有处理该信号的代码,那么大部分默认动作为退出该进程
(2)忽略此信号(除了SIGKILL和SIGSTOP)
(3)捕捉该信号:写一个信号处理函数,信号来的时候,用那处理函数处理。同理,不管如何手动处理SIGKILL和SIGSTOP,它们还是会执行缺省动作
Unix,Linux体系结构,信号编程初步
(1)进程收到信号的时候,内核会注意到这件事,会做出相应处理,比如不再执行缺省动作等
2.捕获信号并处理
(1)signal(信号,信号处理函数指针); //注册信号处理函数
(2)例子:
(3)解析:
(a)用户态和内核态的转换
(b)nginx进程收到信号后,该进程从用户态切换到内核态,在内核态里调用一些处理函数,内核发现在代码里对该信号注册了一个信号处理函数,那么又从内核态先切换回用户态来调用该信号处理函数。处理完后又切换回内核态来做一些收尾工作,最后切换回用户态,继续执行后面的流程
3.可重入函数
(1)概念:某函数,在信号处理函数中被调用,若结果是安全的,那么该函数是可重入函数,也称为异步信号安全的
(2)结果是安全的:比如某函数中要修改全局变量的值,而全局变量可能在别的线程中被使用,那么很可能出错
4.写信号处理函数的注意事项
(1)尽量使用简单的语句做简单的事情,尽量不要调用系统函数
(2)如果非要调用系统函数,那么要保证调用的系统函数是可重入的
(3)一些可重入的系统函数如下表:
(4)errno是个Linux系统中定义的int变量,当系统函数调用时出错那么errno的值会做相应的修改。要用errno的话,需要#include
(5)如果必须要在信号处理函数中调用那些可能修改errno值的可重入的系统函数,那么解决方案是:事先备份errno的值,在信号处理函数返回之前将其恢复
5.signal因为兼容性,可靠性等一些历史问题,不建议使用。推荐使用sigaction()来代替
6.不可重入函数的错用举例
#include
#include
#include
#include
void sig_handle(int sign)
{
char *p = NULL;
p = (char *)malloc(64);
free(p);
if (sign == SIGTERM)
{
printf("捕捉到了SIGTERM信号\n");
}
else if (sign == SIGHUP)
{
printf("捕捉到了SIGHUP信号\n");
}
}
int main(int arg, const char *argv)
{
char *p = NULL;
printf("I am learning Linux C++ Server lesson !\n");
if (signal(SIGTERM, sig_handle) == SIG_ERR)
{
printf("无法捕捉SIGTERM信号\n");
}
if (signal(SIGHUP, sig_handle) == SIG_ERR)
{
printf("无法捕捉SIGHUP信号\n");
}
while (1)
{
p = (char *)malloc(64);
free(p);
}
printf("Application has quited!\n");
return 0;
}
解析:
(1)malloc是不可重入函数,在main函数的死循环里调用了malloc函数,在信号处理函数sig_handle中也调用了malloc函数。当发信号给nginx进程的时候,信号处理函数被调用,那么可能会出现:main函数中的malloc函数调用还没返回,在信号处理函数中又调用malloc函数,那么既可能出错。
(2)如图,出现了错误,后面再发信号给nginx进程,再也没有正常处理
信号编程进阶,sigprocmask
1.若同时发多个相同的信号给进程,进程中对应的信号处理函数第一次被调用后若没有执行完,那么后面的多个相同的信号会等着,直到该信号处理函数执行完返回了才会再次调用该信号处理函数。即不会同时多次调用同一个信号的信号处理函数。
2.信号集
(1)一个进程,必须能够记住这个进程当前阻塞了哪些信号。我们需要 “信号集 ”的这么一种数据类型(结构体),能够把这60多个信号都表示下(都装下)。
(2)信号集的定义:信号集表示一组信号的来(1)或者没来(0)
(3)linux 是用sigset_t结构类型来表示信号集的;
typedef struct{
unsigned long sig[2];
}sigset_t;
(4)一个进程拥有一个自己的信号集,用来记录当前屏蔽(阻塞)了哪些信号。如果我们把这个信号集中的某个信号位设置为1,就表示屏蔽了同类信号,此时再来个同类信号,那么同类信号会被屏蔽,不能传递给进程;如果这个信号集中有很多个信号位都被设置为1,那么所有这些被设置为1的信号都是属于当前被阻塞的而不能传递到该进程的信号
3.信号集的相关函数
(1)sigemptyset(sigset_t指针):把信号集中的所有信号都清0,表示这60多个信号没有来
(2)sigfillset(sigset_t指针):把信号集中的所有信号都设置为1,跟sigemptyset()正好相反
(3)sigaddset(sigset_t指针):往信号集中增加信号
(4)sigdelset(sigset_t指针):从信号集中删除特定信号
(5)sigismember(sigset_t指针,信号名):测试指针所指向的信号集中的信号名对应的信号位是否被置位
(6)sigprocmask(标志,sigset_t指针set,sigset_t指针old)
(a)标志有三个:
1)SIG_BLOCK:将set所指向的信号集中包含的信号加到当前的信号掩码中。即信号掩码和set信号集进行或操作
2)SIG_UNBLOCK:将set所指向的信号集中包含的信号从当前的信号掩码中删除。即信号掩码和set进行与操作。
3)SIG_SETMASK :将set的值设定为新的进程信号掩码。即set对信号掩码进行了赋值操作。
(b)第三个参数不为空,则进程老的(调用本sigprocmask()之前的)信号集会保存到第三个参数里,用于后续,这样后续可以恢复老的信号集给线程
(7)例子:
#include
#include
#include
#include
//信号处理函数
void sig_quit(int signo)
{
printf("收到了SIGQUIT信号!\n");
if (signal(SIGQUIT, SIG_DFL) == SIG_ERR)
{
printf("无法为SIGQUIT信号设置缺省处理(终止进程)!\n");
exit(1);
}
}
int main(int argc, char *const *argv)
{
//信号集,新的信号集,原有的信号集,挂起的信号集
sigset_t newmask, oldmask;
//注册信号对应的信号处理函数,"ctrl+\"
if (signal(SIGQUIT, sig_quit) == SIG_ERR)
{
printf("无法捕捉SIGQUIT信号!\n");
exit(1);
}
//newmask信号集中所有信号都清0(表示这些信号都没有来);
sigemptyset(&newmask);
//设置newmask信号集中的SIGQUIT信号位为1,即再来SIGQUIT信号时,进程就收不到,设置为1就是该信号被阻塞掉
sigaddset(&newmask, SIGQUIT);
//sigprocmask():设置该进程所对应的信号集
//第一个参数用了SIG_BLOCK表明设置 进程 新的信号屏蔽字 为 当前信号屏蔽字 和 第二个参数指向的信号集 的并集
//一个 “进程” 的当前信号屏蔽字,刚开始全部都是0的;所以相当于把当前 "进程"的信号屏蔽字设置成 newmask(屏蔽了SIGQUIT);
//第三个参数不为空,则进程老的(调用本sigprocmask()之前的)信号集会保存到第三个参数里,用于后续,这样后续可以恢复老的信号集给线程
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
{
printf("sigprocmask(SIG_BLOCK)失败!\n");
exit(1);
}
printf("我要开始休息10秒了--------begin--,此时我无法接收SIGQUIT信号!\n");
sleep(10); //这个期间无法收到SIGQUIT信号的;
printf("我已经休息了10秒了--------end----!\n");
//测试一个指定的信号位是否被置位(为1),测试的是newmask
if (sigismember(&newmask, SIGQUIT))
{
printf("SIGQUIT信号被屏蔽了!\n");
}
else
{
printf("SIGQUIT信号没有被屏蔽!!!!!!\n");
}
//测试另外一个指定的信号位是否被置位,测试的是newmask
if (sigismember(&newmask, SIGHUP))
{
printf("SIGHUP信号被屏蔽了!\n");
}
else
{
printf("SIGHUP信号没有被屏蔽!!!!!!\n");
}
//现在我要取消对SIGQUIT信号的屏蔽(阻塞)--把信号集还原回去
//第一个参数用了SIGSETMASK表明设置 进程 新的信号屏蔽字为 第二个参数 指向的信号集,第三个参数没用
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
{
printf("sigprocmask(SIG_SETMASK)失败!\n");
exit(1);
}
printf("sigprocmask(SIG_SETMASK)成功!\n");
//测试一个指定的信号位是否被置位,这里测试的当然是oldmask
if (sigismember(&oldmask, SIGQUIT))
{
printf("SIGQUIT信号被屏蔽了!\n");
}
else
{
printf("SIGQUIT信号没有被屏蔽,您可以发送SIGQUIT信号了,我要sleep(10)秒钟!!!!!!\n");
int mysl = sleep(10);
if (mysl > 0)
{
//sleep()函数能够被打断
//来了某个信号,使sleep()提前结束,此时sleep会返回一个值,这个值就是未睡够的时间
printf("sleep还没睡够,剩余%d秒\n", mysl);
}
}
printf("再见了!\n");
return 0;
}
fork函数详解,范例演示
1.fork()
(1)作用:创建子进程。
(2)子进程从fork()返回处开始执行与父进程相同的代码,且它们共享同一段内存空间
(3)fork()返回后,是父进程还是子进程先执行后面的代码是不确定的,跟内核进程调度算法有关
(4)调用一次fork()会返回两次,即分别从父进程、子进程中返回,所以父进程得到一个pid_t类型值,子进程也得到一个pid_t类型值
(5)对于子进程:fork()成功的话返回0
(6)对于父进程:fork()成功的话返回新建立的子进程的pid
(7)练习:((fork() && fork()) || (fork() && fork())) ,结果一共有7个进程
2.僵尸进程
(1)来源:一个子进程结束了,但它的父进程还活着,但父进程没调用wait/waitpid等函数进程额外处理该子进程的结束信号,子进程就变成僵尸进程。在ps的state中的标记为Z
(2)僵尸进程也占系统资源
(3)kill -9 子进程pid 后,父进程会收到SIGCHLD信号
3.处理僵尸进程
(1)在父进程中写个信号处理函数处理SIGCHLD信号,在信号处理函数中调用waitpid函数
(2)例如:
#include
#include //malloc,exit
#include //fork
#include
#include //waitpid
//信号处理函数
void sig_usr(int signo)
{
int status;
switch(signo)
{
case SIGUSR1:
printf("收到了SIGUSR1信号,进程id=%d!\n",getpid());
break;
case SIGCHLD:
printf("收到了SIGCHLD信号,进程id=%d!\n",getpid());
//新函数waitpid,有人也用wait,掌握和使用waitpid即可;
//这个waitpid说白了获取子进程的终止状态
//只要父进程获取了子进程的状态信息
//那么内核就认为该子进程(僵尸进程)被处理好了,可以退出
//第一个参数为-1,表示等待任何子进程
//第二个参数:保存子进程的状态信息
//第三个参数:提供额外选项,WNOHANG表示(wait no hang)不要阻塞,让这个waitpid()立即返回
pid_t pid = waitpid(-1,&status,WNOHANG);
//子进程没结束,会立即返回这个数字,但这里应该不是这个数字
if(pid == 0)
return;
//这表示这个waitpid调用有错误,有错误也理解返回出去,我们管不了这么多
if(pid == -1)
return;
return;
break;
}
}
int main(int argc, char* const*argv)
{
pid_t pid;
printf("进程开始执行!\n");
if(signal(SIGUSR1,sig_usr) == SIG_ERR)
{
printf("无法捕捉SIGUSR1信号!\n");
exit(1);
}
if(signal(SIGCHLD,sig_usr) == SIG_ERR)
{
printf("无法捕捉SIGCHLD信号!\n");
exit(1);
}
pid = fork();
//要判断子进程是否创建成功
if(pid < 0)
{
printf("子进程创建失败,很遗憾!\n");
exit(1);
}
//现在,父进程和子进程同时开始运行了
for(;;)
{
sleep(1); //休息1秒
printf("休息1秒,进程id=%d!\n",getpid());
}
printf("再见了!\n");
return 0;
}
(3)对于waitpid函数的第一个参数
(a)pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
(b)pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
(c)pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
(d)pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
4.父和子进程的共享内存空间
(1)特性:写时复制
(2)写时复制:父、子进程可以同时读取该内存,但如果父或子进程要对该进程修改的话,那么该段内存就会被复制一份给要修改该内存的那个进程
(3)额外结论:fork()函数的调用返回是非常快的,因为fork()的时候不会复制内存,要修改的时候才会复制
5.fork()可能失败
(1)原因一:该时刻系统里的进程数太多,系统资源不足
(2)原因二:一个用户能fork()的进程数是有限的
(3)查看一个用户可以创建的进程数
调用函数sysconf(_SC_CHILD_MAX);
守护进程详解,范例演示
1.普通进程
(1)进程有对应的终端,如果终端退出,那么对应的进程也就消失了
2.守护进程
(1)一种长期运行[不是必须,但一般应该这样做]的进程,这种进程在后台运行,并且不跟任何的控制终端关联
(2)守护进程是在后台运行,不会占着终端,终端可以执行其他命令
3.Linux的守护进程
(1)linux操作系统本身是有很多的守护进程在默默的运行,维持着系统的日常活动。大概30-50个
(2)常见:
a)ppid = 0:内核进程,跟随系统启动而启动,声明周期贯穿整个系统
b)cmd列名字带[]这种,叫内核守护进程
c)老祖init:也是系统守护进程,它负责启动各运行层次特定的系统服务;所以很多进程的PPID是init。而且这个init也负责收养孤儿进程
d)cmd列中名字不带[]的普通守护进程(用户级守护进程)
(3)共同点总结:
a)大多数守护进程都是以超级用户特权运行的
b)守护进程没有控制终端,TT这列显示?
c)内核守护进程以无控制终端方式启动
d)普通守护进程可能是守护进程调用了setsid的结果(无控制端)
(4)例子:
a)ps的e参数表示显示所有进程;
f参数表示显示完整的列;
j参数表示也显示任务或者作业
4.文件描述符
(1)正数,用来标识一个文件
(2)当你打开一个存在的文件或者创建一个新文件,操作系统都会返回这个文件描述符(其实就是代表这个文件的),后续对这个文件的操作的一些函数,都会用到这个文件描述符作为参数
(3)linux中三个特殊的文件描述符,数字分别为0,1,2
0:标准输入【键盘】,对应的符号常量叫STDIN_FILENO
1:标准输出【屏幕】,对应的符号常量叫STDOUT_FILENO
2:标准错误【屏幕】,对应的符号常量叫STDERR_FILENO
(4)类Unix操作系统,默认从STDIN_FILENO读数据,向STDOUT_FILENO来写数据,向STDERR_FILENO来写错误;同时,你程序一旦运行起来,这三个文件描述符0,1,2会被自动打开(自动指向对应的设备)
(5)类Unix操作系统有个说法:一切皆文件,所以它把标准输入,标准输出,标准错误都看成文件。
与其说把标准输入,标准输出,标准错误 都看成文件,到不如说:
像看待文件一样看待标准输入,标准输出,标准错误;
像操作文件一样操作标准输入,标准输出,标准错误
5.输入输出重定向
(1)输入重定向:不从键盘输入了,从别的地方(文件等)输入
如:cat < myinfile ,将myinfile文件的内容做为cat命令的输入
(2)输出重定向:不输出到屏幕,输出到别的地方(如文件等)
如:ls -ltr > myoutfile ,将ls -ltr命令的输出弄到myoutfile文件中去,不显示在屏幕上
(3)空设备(黑洞):/dev/null ,这个目录是一个特殊的设备文件,它丢弃一切写入其中的数据(象黑洞一样)
6.守护进程编写规则
(1)先调用函数umask(0); 用来限制(屏蔽)一些文件权限的
(2)fork()一个子进程(脱离终端)出来,然后父进程退出( 把终端空出来,不让终端卡住)
(3)fork()的目的是想成功调用setsid()来建立新会话,目的是子进程有单独的sid;而且子进程也成为了一个新进程组的组长进程;同时,子进程不关联任何终端了
(4)把守护进程的 标准输入,标准输出,重定向到空设备(/dec/null黑洞),从而确保守护进程不从键盘接收任何东西,也不把输出结果打印到屏幕
(5)实现范例
#include
#include
#include
#include
#include //umask()
#include //open()
//创建守护进程
//创建成功则返回1,否则返回-1
int ngx_daemon()
{
int fd;
//fork()子进程
switch (fork())
{
case -1:
//创建子进程失败,这里可以写日志......
return -1;
case 0:
//子进程,走到这里,直接break;
break;
default:
//父进程,直接退出
exit(0);
}
//只有子进程流程才能走到这里
//脱离终端,终端关闭,将跟此子进程无关
if (setsid() == -1)
{
//记录错误日志......
return -1;
}
//设置为0,不要让它来限制文件权限,以免引起混乱
umask(0);
//打开黑洞设备,以读写方式打开
fd = open("/dev/null", O_RDWR);
if (fd == -1)
{
//记录错误日志......
return -1;
}
//dup2函数说明
//int dup2(int oldfd,targetfd);
//如果targetfd的文件描述符已被程序使用,则将其关闭
//复制oldfd的文件描述符,将targetfd的文件描述符指向oldfd的拷贝
//即起到【重定向】的作用,将标准输入重定向到oldfd指向的文件
if (dup2(fd, STDIN_FILENO) == -1)
{
//记录错误日志......
return -1;
}
if (dup2(fd, STDOUT_FILENO) == -1)
{
//记录错误日志......
return -1;
}
//fd应该是3,这个应该成立
if (fd > STDERR_FILENO)
{
//释放资源这样这个文件描述符就可以被复用
//不然这个数字【文件描述符】会被一直占着
//导致内存不够
if (close(fd) == -1)
{
//记录错误日志......
return -1;
}
}
return 1;
}
int main(int argc, char *const *argv)
{
if(ngx_daemon() != 1)
{
//创建守护进程失败,可以做失败后的处理比如写日志等等
return 1;
}
else
{
//创建守护进程成功,执行守护进程中要干的活
for(;;)
{
sleep(1);
//现在标准输出指向黑洞(/dev/null),打印的结果不会显示在屏幕上
printf("休息1秒,进程id=%d!\n",getpid());
}
}
return 0;
}
7.守护进程不会收到的信号
(1)不会收到的信号是指由系统内核发出的,而不是别的进程发出的
(2)不会收到SIGHUP信号
(a)很多守护进程把这个信号作为通知信号,表示配置文件已经发生改动,守护进程应该重新读入其配置文件
例子:如第四课中的sudo ./nginx -s reload,重新加载worker进程,这个过程中master进程是不会退出的。master进程是个守护进程,我们通过在命令行(bash进程)执行sudo ./nginx -s reload,其实是给用非内核给它发送SIGHUP信号,那么在nginx的master对应的SIGHUP信号处理代码中,行为是结束现在所有的worker进程,然后重新加载配置文件,并重新启动所有worker进程(worker进程前后的pid都变了)。
推论:sudo kill -1 master进程pid 的效果跟 sudo ./nginx -s reload 的效果是一样的
(3)不会收到SIGINT(ctrl+C),SIGWINCH(终端窗口大小改变) 信号
服务器程序目录规划,makefile编写
1.目录规划(自己重新规划的,跟原始资料不同)
(1)分为各个模块,一个模块对应一个模块文件夹
(2)模块文件夹内分两个文件夹,include文件夹专门存放头文件,src文件夹专门存放源文件
(3)app模块为main函数所在处,同时做一些nginx的初始化配置等工作
(4)common文件夹下有include文件夹,那里存放各个模块的公共接口,即各个模块的头文件都复制一份放那里,提供公共函数接口给各个模块使用
(5)各个模块文件夹下有模块Makefile文件
(6)总目录下有总工程Makefile文件
(7)make后生成build文件夹,里面再分别给各个模块生成一个独立文件夹,里面的dep子文件夹存放依赖文件信息,obj子文件夹存放.o目标文件
(8)初始如图
<========== 模块Makefile文件 ==========>
.PHONY : all rebuild clean
TYPE_INC := .h
TYPE_SRC := .cpp
TYPE_OBJ := .o
TYPE_DEP := .dep
TYPE_LIB := .a
MODULE := $(abspath .)
MODULE := $(notdir $(MODULE))
DIR_INC := include
DIR_SRC := src
DIR_BUILD_MODULE := $(DIR_BUILD)/$(MODULE)
DIR_OBJ := $(DIR_BUILD_MODULE)/obj
DIR_DEP := $(DIR_BUILD_MODULE)/dep
DIR_TO_CREATE := $(DIR_BUILD_MODULE) $(DIR_OBJ)
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
MKDIR := mkdir
MKDIR_FLAGS := -p
RM := rm -rf
CC := g++
CFLAGS := -I $(DIR_INC) -I $(DIR_COMMON_INC)
AR := ar
ARFLAGS := crs
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
SRC := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJ := $(SRC:$(TYPE_SRC)=$(TYPE_OBJ))
OBJ := $(patsubst $(DIR_SRC)/%,$(DIR_OBJ)/%,$(OBJ))
DEP := $(SRC:$(TYPE_SRC)=$(TYPE_DEP))
DEP := $(patsubst $(DIR_SRC)/%,$(DIR_DEP)/%,$(DEP))
all : $(DIR_TO_CREATE) $(OBJ)
$(AR) $(ARFLAGS) $(DIR_BUILD)/$(MODULE)$(TYPE_LIB) $(filter %$(TYPE_OBJ),$^)
$(DIR_TO_CREATE) :
$(MKDIR) $(MKDIR_FLAGS) $@
ifeq ($(MAKECMDGOALS),all)
-include $(DEP)
endif
ifeq ($(MAKECMDGOALS),)
-include $(DEP)
endif
$(DIR_OBJ)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
ifeq ($(wildcard $(DIR_DEP)),)
$(DIR_DEP)/%$(TYPE_DEP) : $(DIR_DEP) %$(TYPE_SRC)
else
$(DIR_DEP)/%$(TYPE_DEP) : %$(TYPE_SRC)
endif
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(CFLAGS) $(filter %$(TYPE_SRC),$^) | \
sed 's,\(.*\)\.o[ :]*,$(DIR_OBJ)/\1.o $@ : ,g' > $@
$(DIR_DEP) :
$(MKDIR) $(MKDIR_FLAGS) $@
clean :
$(RM) $(DIR_BUILD_MODULE)
rebuild :
$(MAKE) clean
$(MAKE) all
<========== 总工程Makefile文件 ==========>
.PHONY : all rebuild clean
MODULE := app
MKDIR := mkdir
RM := rm -rf
CC := g++
TYPE_LIB := .a
DIR_ROOT := $(abspath .)
DIR_BUILD := $(DIR_ROOT)/build
DIR_COMMON := $(DIR_ROOT)/common
DIR_COMMON_INC := $(DIR_COMMON)/include
DIR_TO_CREATE := $(DIR_BUILD)
LIB := $(addprefix $(DIR_BUILD)/,$(MODULE))
LIB := $(addsuffix $(TYPE_LIB),$(LIB))
EXE := $(DIR_BUILD)/nginx
DEBUG := true
define moduleCompile
cd $(1) && $(MAKE) all \
DIR_BUILD:=$(DIR_BUILD) \
DEBUG:=$(DEBUG) \
DIR_COMMON_INC:=$(DIR_COMMON_INC) \
&&cd ..;
endef
all : $(DIR_TO_CREATE) compile link
@echo "Success!Target ==> $(EXE)"
compile :
@echo "Begin to compile ..."
for module in $(MODULE); \
do \
$(call moduleCompile,$$module) \
done
@echo "End compile ..."
link : $(LIB)
@echo "Begin to link ..."
$(CC) -o $(EXE) -Xlinker "-(" $^ -Xlinker "-)"
@echo "End link ..."
$(DIR_TO_CREATE) :
@echo "Creating directory $@ ..."
@$(MKDIR) $@
$(MODULE) : $(DIR_BUILD)
@echo "Begin to compile $@ ..."
$(call moduleCompile,$@)
@echo "End compile $@"
clean :
$(RM) $(DIR_TO_CREATE)
rebuild :
$(MAKE) clean
$(MAKE) all
读配置文件,查泄露,设置标题实战
1.读配置文件
(1)约定,每行配置内容不超过500字符
(2)配置文件放在nginx工程总目录下的nginx.conf文件里
(3)具体看项目代码
2.查泄露
(1)需要用到valgrind工具
(2)sudo apt-get install valgrind
(3)编译的时候要加上-g选项(修改makefile文件)
(4)使用方法
(a)格式
valgrind --tool=memcheck 一些开关 可执行文件名
–tool=memcheck :使用valgrind工具集中的memcheck工具
–leak-check=full : 指的是完全full检查内存泄漏
–show-reachable=yes :是显示内存泄漏的地点
–trace-children = yes :是否跟入子进程
–log-file=log.txt:讲调试信息输出到log.txt,不输出到屏幕
(b)到nginx可执行文件所在的目录下执行:
valgrind --tool=memcheck --leak-check=full --show-reachable=yes ./nginx
3.设置进程标题
(1)必备概念
①进程的标题是在main函数的argv参数里的
②argv内存之后,接着连续的就是环境变量参数信息内存(是咱们这个可执行程序执行时有关的所有环境变量参数信息)
③环境变量参数信息可以通过一个全局的environ变量(类型为char **)访问
④environ内存和argv内存紧紧的挨着,从argv指向的内存开始,到environ指向的内存结束,中间那段内存是连续的
(2)实现思路
①要将environ指向的内存里的内容搬到一个自己new出来的内存里,重新调整environ[i]的指向
②修改argv[0]所指向的内容
③将argv[1]置空
④将除了存放标题之外的内存置零
日志打印实战,优化main函数调用顺序
1.在switch里进行continue时,continue不是作用在switch,而是作用在外层循环!!!
2.修改时区
(1)tzselect
(2)4
(3)9
(4)1
(5)1
(6)上述操作产生了一个北京时间的临时文件,这时,重启虚拟机,上述修改无法保留
解决方法:
sudo cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
3.日志模块的构建
(1)核心思想:类比printf支持各种格式,支持可变参,重新定制一组自己的日志输出函数
(2)难点:处理%f比较复杂,涉及位运算,进制转换,进位问题等
(3)核心函数:(相关语句)
①ngx_log.fd = open((const char*)pname,O_WRONLY|O_APPEND|O_CREAT,0644);
a)0644表示文件权限,0表示十进制
b)用了open,那么一定要调用close(ngx_log.fd)来回收文件描述符
②char* perrorinfo = strerror(err);
a)strerror是系统函数,err是int类型,根据err的值,返回一个对该错误类型的字符串描述
③int n = write(STDERR_FILENO,errstr,p - errstr);
a)往文件描述符STDERR_FILENO指向的文件里写内容,内容为errstr指向的字符 指针,要写入的字节数为p - errstr
④ struct timeval tv;
struct tm tm;
time_t sec; //秒
u_char *p; //指向当前要拷贝数据到其中的内存位置
va_list args;
memset(&tv,0,sizeof(struct timeval));
memset(&tm,0,sizeof(struct tm));
gettimeofday(&tv, NULL); //获取当前时间,返回自1970-01-01 00:00:00到现在经历的秒数【第二个参数是时区,一般不关心】
sec = tv.tv_sec; //秒
localtime_r(&sec, &tm); //把参数1的time_t转换为本地时间,保存到参数2中去, //带_r的是线程安全的版本,尽量使用
tm.tm_mon++; //月份要调整下才正常
tm.tm_year += 1900; //年份要调整下才正常
(4)相关结构体
a)struct timeval {
time_t tv_sec; // 秒
long tv_usec; // 微妙
};
b)struct tm {
int tm_sec; //秒 – 取值区间为[0,59]
int tm_min; // 分 - 取值区间为[0,59]
int tm_hour; // 时 - 取值区间为[0,23]
int tm_mday; // 一个月中的日期 - 取值区间为[1,31]
int tm_mon; // 月份(从一月开始,0代表一月) - 取值区间为[0,11]
int tm_year; // 年份,其值等于实际年份减去1900
int tm_wday; // 星期 – 取值区间为[0,6],其中0代表星期天,1代表星 //期一,以此类推
int tm_yday; // 从每年的1月1日开始的天数 – 取值区间为[0,365],其 //中0代表1月1日,1代表1月2日,以此类推
int tm_isdst; // 夏令时标识符,实行夏令时的时候,tm_isdst为正。不实行 //夏令时的进候,tm_isdst为0;不了解情况时,tm_isdst()为负。
};
c)typedef long time_t;
d) #ifdef _WIN64
typedef unsigned __int64 uintptr_t;
#else
typedef unsigned int uintptr_t;
#endif
(5)其他详细内容请看代码
信号,子进程实战,文件IO详谈
1.注册信号处理函数
(1)用sigactionn函数,不用signal函数
(2)int sigaction(int signum, const struct sigaction* act,struct sigaction* oldact);
a)signum参数指出要捕获的信号类型,act参数指定新的信号处理方式,oldact参数输 出先前信号的处理方式(如果不为NULL的话)
(3)struct sigaction结构体介绍
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
①sa_handler:此参数和signal()的参数handler相同,此参数主要用来对信号旧的安 装函数signal()处理形式的支持
②sa_sigaction:新的信号安装机制,处理函数被调用的时候,不但可以得到信号编号,而且可以获悉被调用的原因以及产生问题的上下文的相关信息。
③sa_mask:用来设置在处理该信号时暂时将sa_mask指定的信号搁置
④sa_restorer: 此参数没有使用
⑤sa_flags:用来设置信号处理的其他相关操作,下列的数值可用,可用OR 运算(|) 组合:
a)A_NOCLDSTOP:如果参数signum为SIGCHLD,则当子进程暂停时并不会通知父 进程;
b)SA_ONESHOT/SA_RESETHAND:当调用新的信号处理函数前,将此信号处理方式 改为系统预设的方式;
c)SA_RESTART:被信号中断的系统调用会自行重启;
d)SA_NOMASK/SA_NODEFER:在处理此信号未结束前不理会此信号的再次到来;
e)SA_SIGINFO:信号处理函数是带有三个参数的sa_sigaction,即设置信号处理函 数为sa_sigaction指针成员指向的函数
2.kill -9 -组进程id 可以杀死同一组里的进程
3.sigsuspend函数
(1)参数为sigset_t对象指针
(2)内部工作流程:
a)根据给定的参数设置新的mask 并阻塞当前进程,阻塞的时候不占用CPU资源。
b)当收到了mask里没有屏蔽的信号时,恢复进程原先的信号屏蔽(在调用sigsuspend函数前屏蔽的那些信号都会被重新屏蔽,以免执行后面流程的时候被打断)
c)调用收到的信号的相应的信号处理函数
d)信号处理函数返回后sigsuspend函数才返回
e)执行sigsuspend函数后面的代码
4.关于回车
(1)\r:回车符,把打印【输出】信息的为止定位到本行开头
(2)\n:换行符,把输出为止移动到下一行
(3)一般把光标移动到下一行的开头: \r\n
(4)比如windows下,每行结尾 \r\n
(5)类Unix,每行结尾就只有 \n
(6)Mac苹果系统,每行结尾只有 \r
(7)结论:统一用 \n
5.关于printf()函数不加\n无法及时输出的解释
(1)因为在类Unix里有行缓存,在windows中没有
(2)需要输出的数据不直接显示到终端,而是首先缓存到某个地方,当遇到行刷新表指或者该缓存已满的情况下,菜会把缓存的数据显示到终端设备
(3)ANSI C中定义\n认为是行刷新标记,所以,printf函数没有带\n是不会自动刷新输出流,直至行缓存被填满才显示到屏幕上
(4)一些解决方案
a)fflush(stdout);
b)setvbuf(stdout,NULL,_IONBF,0); //这个函数直接将printf缓冲区禁止,printf就 直 接输出
6.关于多个进程同时write的思考
(1)经过测试,当前nginx项目里的多个进程同时对一个文件进行write时,文件内容没有发生混乱
(2)这里有个前提:这些进程是有父子关系的,即在master进程里进行open文件,在master进程或者worker进程中进行write文件
(3)当进程间有亲缘关系,那么会共享文件表项
(4)但是如果在没有亲缘关系的多个进程中同时对一个文件进行write,那可能会造成数据混乱
7.关于write函数的写入安全问题
(1)write是原子操作
(2)在应用程序里调用write返回后,系统只是把数据从应用程序缓冲区调到内核缓冲区,但是并不一定马上就会把内核缓冲区里的数据写入磁盘
(3)所以,掉电时数据可能还没有写入磁盘,造成数据丢失
(4)解决方案
a)直接访问物理磁盘,绕过内核缓冲区:在调用open函数的时候加上参数O_DIRECT (不推荐,因为效率低)
b)write函数等待数据写入物理磁盘才返回,即数据调到内核缓冲区时立刻写入磁盘: 在调用open函数的时候加上参数O_SYNC(同步)(不推荐,因为效率低)
c)调用sync(void); -->将所有修改过的块缓冲区排入写队列,然后返回,并不等待实 际写磁盘操作结束,数据是否写入磁盘并没有保证(推荐)
d)调用fsync(int fd):将fd对应的文件的块缓冲区立即写入磁盘,并等待实际写磁盘操 作结束返回(推荐)
e)fdatasync(int fd):类似于fsync,但只影响文件的数据部分。而fsync不一样,fsync 除数据外,还会同步更新文件属性(如修改日期,文件大小等)(推荐)
(5)注意:不要调用write一次后就调用一次fsync或fdatasync,而是write了一定量的字节(一般是4K)后才调用一次fsync或fdatasync,因为fsync或fdatasync函数很耗时,经常调用的话程序效率很低
(6)相关图解
8.标准IO库和系统调用
(1)标准IO函数如:fopen,fclose,fread,fwrite等,在内部会有一个CLib缓冲区
(2)系统函数如:open,write等。有一句话:系统调用都是原子性的
(3)相关图解
9.生成一个master进程和N个worker子进程的思路
(1)在最开始的进程里fork一个子进程出来当作master进程
(2)master进程调用setsid函数来变成守护进程
(3)master进程的父进程退出
(4)自定义一个master进程的消息循环函数(一个函数里有死循环,在死循环里不断等待 收到信号),在进入死循环之前fork出相应数量的worker子进程
(5)每一个worker进程被fork出来后也都进入自己相应的消息循环函数
(6)写信号处理函数来处理SIGCHLD信号,防止worker进程被杀掉后变成僵尸进程
(7)相关函数
11.本课新函数(补充前面没写的)
(1)在信号处理函数中,调用waitpid(-1, &status, WNOHANG)后,可有如下逻辑
守护进程及信号处理函数
1.关于守护进程的屏幕输出
(1)守护进程如果通过键盘执行可执行文件来启动,那虽然守护进程与具体终端是脱钩的,但是依旧可以往标准错误上输出内容,这个终端对应的屏幕上可以看到输入的内容
(2)但是如果这个nginx守护进程不是通过终端启动,可能开机就启动,那么这个nginx守护进程就完全无法往任何屏幕上显示信息了,这个时候,要排错就要靠日志文件
2.关于信号处理函数
(1)在第15课的项目代码中已经实现
(2)编写信号处理函数的注意事项
a)代码尽可能简单,尽可能快速的执行完毕返回
b)用一些全局量做一些标记,尽可能不调用函数,以免阻塞其他信号的到来,甚至阻 塞整个程序执行流程
CS,TCP,IP协议妙趣横生,唯妙唯俏谈
1.OSI(Open System Interconnect)七层网络模型
(1)物理层,链路层,网络层,传输层,会话层,表示层,应用层
2.TCP/IP协议四层模型
(1)Transfer Control Protocol[传输控制协议]/Internet Protocol[网际协议]
(2)
3.socket套接字
(1)是个数字,通过调用socket()函数来生成
(2)这个数字具有唯一性
(3)调用close()函数后才把这个数字关闭
(4)可以类比文件描述符,对socket进行write和read时就像对文件进行write和read一 样
TCP三次握手详析,telnet,wireshark示范
1.最大传输单元MTU
(1)MTU(Maximum Transfer Unit)
(2)每个数据包包含的数据最多可以有多少个字节(1.5K左右)
2.TCP连接的三次握手
(1)客户端程序调用connect函数后触发三次握手
(2)过程:
a)客户端发送包含了syn置位的数据包给服务器
b)服务器发送包含了syn和ack置位的数据包给客户端
c)客户端发送包含了ack置位的数据包给客户端
3.TCP连接的三次握手很大程度上是为了防止大量伪造的ip地址和端口对服务器进行攻击
4.TCP断开的四次挥手
(1)调用close函数关闭一个socket后触发四次挥手
(2)过程:
a)FIN,ACK 服务器->客户端
b)ACK 客户端->服务器
c)FIN,ACK 客户端->服务器
d)ACK 服务器->客户端
5.telnet的使用
(1)用来测试ip地址上的某个端口是否练得通
(2)用法:telnet ip地址 端口号
6.wireshark的使用
(1)安装时需要勾选WinpCap
(2)使用时先选一个需要抓包的网卡
(3)根据需要输入特定过滤指令,如
host 192.168.1.111 and port 666
TCP状态转换,TIME_WAIT,SO_REUSEADDR
1.相关演示代码(仅作演示,不计缺陷)
<========== 服务端程序代码 ==========>
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SERV_PORT 9000
int main(int argc, char *const *argv)
{
//服务器的socket套接字【类似于文件描述符】
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
//服务器的地址结构体
struct sockaddr_in serv_addr;
memset(&serv_addr,0,sizeof(serv_addr));
//设置本服务器要监听的地址和端口,这样客户端才能连接到该地址和端口并发送数据
//选择协议族,AF_INET表示IPV4
serv_addr.sin_family = AF_INET;
//绑定我们自定义的端口号,客户端程序和我们服务器程序通讯时,就要往这个端口连接和传送数据
serv_addr.sin_port = htons(SERV_PORT);
//监听本地所有的IP地址;INADDR_ANY表示的是一个服务器上所有的网卡(服务器可能不止一个网卡)多个本地ip地址都进行绑定端口号,进行侦听
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
int result;
//绑定服务器地址结构体
result = bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
if(result == -1)
{
char *perrorinfo = strerror(errno);
printf("bind返回的值为%d,错误码为:%d,错误信息为:%s;\n",result,errno,perrorinfo);
return -1;
}
//参数2表示服务器可以积压的未处理完的连入请求总个数,客户端来一个未连入的请求,请求数+1,连入请求完成,c/s之间进入正常通讯后,请求数-1
result = listen(listenfd, 32);
if(result == -1)
{
char *perrorinfo = strerror(errno);
printf("listen返回的值为%d,错误码为:%d,错误信息为:%s;\n",result,errno,perrorinfo);
return -1;
}
int connfd;
const char *pcontent = "I sent sth to client!\n";
for(;;)
{
//调用accept函数后,会卡在这里,等客户端连接,客户端连入后,该函数走下去
//【注意这里返回的是一个新的socket——connfd,后续本服务器就用connfd和客户端之间收发数据,而原有的lisenfd依旧用于继续监听其他连接】
connfd = accept(listenfd, (struct sockaddr*)NULL, NULL);
//发送数据包给客户端
//注意第一个参数是accept返回的connfd套接字
write(connfd,pcontent,strlen(pcontent));
printf("本服务器给客户端发送了一串字符~~~~~~~~~~~!\n");
//只给客户端发送一个信息,然后直接关闭套接字连接;
close(connfd);
} //end for
return 0;
}
2.netstat命令
(1)作用:显示网络相关信息
(2)一些选项
a)-a:显示所有选项
b)-n:能显示成数字的内容全部显示成数字
c)-p:显示端口或者对应程序名
(3)例如:netstat -anp | grep -E ‘State|9000’
3.相同IP地址的相同端口,只能被bind一次,第二次bind会失败
(1)实验
a)现在第一次运行了服务端程序
如图,9000端口正在被服务端程序监听
b)现在新建一个终端,在第一个服务端程序执行后没有退出的情况下,尝试在新终端 再次执行服务端程序
如图:错误信息为==> 说明地址已被使用,说明第二次bind失败,是不允许的
4.MSL
(1)数据包最长生命周期
5.在windows下的命令行里用telnet与服务器连接
(1)
(2)
(3)观察服务端的网络状态
发现多了一个TIME_WAIT状态
(4)服务端转到TIME_WAIT状态的原因
a)在服务端的代码里,当有新连接过来的时候,write完就close掉了,这时候会进入 TIME_WAIT状态
(5)TIME_WAIT状态的特点
a)TIME_WAIT状态的保持是有时间限制的,为2MSL,一般为1-4分钟。超过了这个时 间,会自动退出TIME_WAIT状态
b)服务端程序的TCP socket为TIME_WAIT状态时,若杀掉服务器程序再重新启动,会 启动失败,因为bind()函数返回失败,错误信息为:Address already in use
c)启动失败的原因参见TCP状态转换图
6.TCP连接的状态转换
(1)注意:状态转换是针对一个TCP连接(一个TCP socket连接)来说的
(2)如上图,服务器主动关闭连接:ESTABLISHED -> FIN_WAIT1 -> FIN_WAIT2 -> TIME_WAIT
客户端被动关闭:ESTABLISHED -> CLOSE_WAIT -> LAST_ACK
7.TIME_WAIT状态
(1)具有TIME_WAIT状态的TCP连接,就好像一种残留的信息一样。当这种状态存在的时候,服务器程序退出并重新执行会失败,会提示:bind返回的值为-1,错误码为:98,错误信息为:Address already in use
(2)RST标志
a)对于每一个TCP连接,操作系统是要开辟出来一个接收缓冲区和一个发送缓冲区来 处理数据的收和发
b)当我们close一个TCP连接时,如果我们这个发送缓冲区有数据,那么操作系统会 把发送缓冲区里的数据发送完毕,然后再发fin包表示连接关闭
c)反观RST标志:出现这个标志的包一般都表示异常关闭;如果发生了异常,一般都 会导致丢失一些数据包
如果将来用setsockopt(SO_LINGER),发送的就是RST包,此时发送缓冲区的数据会 被丢弃
RST是异常关闭,是粗暴关闭,不是正常的四次挥手关闭,所以如果这么关闭tcp 连接,那么主动关闭的那一方也不会进入TIME_WAIT
(3)引入TIME_WAIT状态【并且处于这种状态的时间为2MSL即1-4分钟】 的原因
a)可靠的实现TCP全双工的终止
因为:如果服务器最后发送的ACK【应答】包因为某种原因丢失了,那么客户端一 定会重新发送FIN。此时,因为服务器端有TIME_WAIT的存在,服务器会重 新发送ACK包给客户端。但是如果没有TIME_WAIT这个状态,那么若客户 端没有收到ACK包,服务器都已经关闭连接了,此时客户端重新发送FIN, 服务器给回的就不是ACK包,而是RST【连接复位】包,从而使客户端没有 完成正常的4次挥手,有可能造成数据包丢失。也就是说,TIME_WAIT有助 于可靠的实现TCP全双工连接的终止
b)允许老的重复的TCP数据包在网络中消逝
因为:一个数据包在网络上的存活时间最长为一个MSL,而TIME_WAIT的时间为 两个MSL。在进入TIME_WAIT状态的时候,若有数据包发送过来,此时的处 理行为是丢弃数据包。所以,在进入TIME_WAIT状态之前,发送过来了但是 还没有到达的数据包,在进入TIME_WAIT状态之后,会在网络中消逝
8.setsockopt函数
(1)用在服务器端,在调用socket()之后,调用bind()之前调用setsockopt
(2)功能:设置socket参数选项
(3)函数原型
int setsockopt(int sock, int level, int optname, const void* optval, socklen_t optlen);
参数说明:
sock:将要被设置或者获取选项的套接字
level:选项所在的协议层,可取的值:
1)SOL_SOCKET:通用套接字选项
2)IPPROTO_IP:IP选项
3)IPPROTO_TCP:TCP选项
optname:需要访问的选项名,可用的选项名有:
optval:指向包含新选项值的缓冲
optlen:现选项的长度
返回说明:
成功执行时,返回0。失败返回-1,errno被设为以下的某个值
EBADF:sock不是有效的文件描述词
EFAULT:optval指向的内存并非有效的进程空间
EINVAL:在调用setsockopt()时,optlen无效
ENOPROTOOPT:指定的协议层不能识别选项
ENOTSOCK:sock描述的不是套接字
用例:
< 修改发送缓冲区大小 >
int nSendBuf=321024;//设置为32K
setsockopt(s,SOL_SOCKET,SO_SNDBUF,(const char)&nSendBuf,sizeof(int));
可知,根据第三个参数的选项类型,决定第二个参数是什么
9.setsocketpt函数的SO_REUSEADDR选项
(1)SO_REUSEADDR的能力:
a)SO_REUSEADDR允许启动一个监听服务器并捆绑其端口,即使以前建立的将端口用作他们的本地端口的连接仍旧存在【即便TIME_WAIT状态存在,服务器bind()也能成功】
b)允许同一个端口上启动同一个服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可
c)SO_REUSEADDR允许单个进程捆绑同一个端口到多个套接字,只要每次捆绑指定不同的本地IP地址即可
d)SO_REUSEADDR允许完全重复的绑定:当一个IP地址和端口已经绑定到某个套接字上时,同样的IP地址和端口还可以绑定到另一个套接字上;一般来说本特性仅支持UDP套接字,而TCP不行
10.所有TCP服务器都应该指定SO_REUSEADDR选项,以防止当套接字处于TIME_WAIT时bind()失败的情形出现
11.修改第一点的代码,即便TIME_WAIT状态存在,服务器bind()也能成功
在调用bind函数前加上其上所示代码
listen()队列剖析,阻塞非阻塞,同步异步
1.listen()队列剖析
(1)int listen(int sockfd, int backlog);
(2)用途:监听端口,用在TCP连接中的服务器端
(3)对于backlog参数,涉及 “监听套接字队列”
2.监听套接字队列
(1)对于一个调用listen()进行监听的套接字,操作系统会给这个套接字维护两个队列
a)未完成连接队列 【保存连接用的】
i. 当客户端发送tcp连接三次握手的第一次【syn包】给服务器的时候,服务器就 会在未完成队列中创建一个跟这个syn包对应的一项
ii. 可以把这项看成是一个半连接【因为连接还没建立起来】,这个半连接的状态 会从LISTEN变成SYN_RCVD状态,同时给客户端返回第二次握手包【syn,ack】
iii.这个时候,其实服务器是在等待完成第三次握手
b)已完成连接队列 【保存连接用的】
i.当第三次握手完成了,连接就变成了ESTABLISHED状态,每个已经完成三次握手 的客户端都放在这个队列中作为一项
(2)如图所示
3.关于backlog
(1)backlog曾经的含义:已完成队列和未完成队列里边条目之和不能超过backlog
(2)现在进一步明确和规定backlog的含义:指定给定套接字上内核为之排队的最大已完 成连接数(已完成连接队列中最大条目数)
(3)一般这个backlog值给300左右
4.RTT
(1)RTT是未完成队列中任意一项在未完成队列中留存的时间,这个时间取决于客户端和 服务器
(2)对于客户端,这个RTT时间是第一次和第二次握手加起来的时间
(3)对于服务器,这个RTT时间实际上是第二次和第三次握手加起来的时间
(4)在三次握手的最后一步中,若服务器迟迟没收到第三个ack包,那么处于SYN_RCVD 的这一项(服务器端的未完成队列中)会一致停留在服务器的未完成队列中,这个停 留时间大概是75秒,如果超过这个时间,这一项会被操作系统干掉
(5)相关图示
5.connect函数返回时机
(1)在收到三次握手的第二次握手包(也就是收到服务器发回来的syn/ack)之后就返回
6.accept函数返回时机
(1)如图所示
服务器在收到三次握手的第三个握手包后accept函数返回
7.accept函数
(1)用来从已完成连接队列中的队首位置取出来一项【每一项都是一个已经完成三路握手 的TCP连接】,返回给进程
(2)如果已完成连接队列是空的,那么accept()会一致卡在当前行【休眠】等待,一直到已 完成队列中有一项时才会被唤醒
8.syn攻击【syn flood】
(1)拒绝服务攻击(DOS/DDOS(分布式DOS))
(2)从编程角度,我们要尽快的用accept()把已完成队列中的数据【TCP连接】取走,尽快 留出空闲为止给后续的已完成三路握手的条目用,那么这个已完成队列就不会满
9.区分服务器监听端口的socket和accept取到的socket
(1)例如服务器用来监听9000端口的这个套接字,叫“监听套接字【listenfd】”,只要服 务器程序在运行,这个套接字就应该一直存在
(2)当客户端连接进来,在服务器这一方的操作系统会为每个成功与服务器建立三次握手 的客户端再创建一个套接字【一个已经连接好的套接字】,accept()返回的就是这种套 接字;也就是从已完成连接队列中取得的一项。随后,服务器使用这个accept()返回的 套接字和客户端通信
10.思考
(1)如果两个队列之和【已完成连接队列,和未完成连接队列】达到了listen()所指定的第 二参数,也就是说队列满了,此时,再有一个客户发送syn请求,服务器怎么反应?
答:服务器会忽略这个syn,不给回应; 客户端这边,发现syn没回应,过一会会重 发syn包
(2)一个TCP连接完成三次握手后被扔到已经完成队列中去,到accept()从已完成队列中 把这个连接取出这个之间是有个时间差的,如果还没等accept()从已完成队列中把这个 连接取走的时候,客户端如果发送来数据,这个数据会怎样?
答:这个数据会被保存在已经连接的套接字的接收缓冲区里,这个缓冲区有多大,最大 就能接收多少数据量
11.阻塞与非阻塞I/O
(1)阻塞和非阻塞主要是指调用某个系统函数时,这个函数是否会导致我们的进程进入sleep()【卡在这休眠】状态而言的
(2)阻塞I/O
a)调用一个函数,这个函数就卡在这里,整个程序流程不往下走了【休眠sleep】,该 函数卡在这里等待一个事情发生,只有这个事情发生了,这个函数才会往下走
b)阻塞效率很低;一般不会用阻塞方式来写服务器程序
c)相关图示
(3)非阻塞I/O
a)调用时如果没有数据到来也不会卡住,直接返回,充分利用时间片,执行效率更高
b)相关图示
(4)非阻塞模式的两个鲜明特点
a)不断的调用accept(),recvfrom()函数来检查有没有数据到来,如果没有,函数会返 回一个特殊的错误标记,这种标记可能是EWULDBLOCK,也可能是EAGAIN;如果数 据没到来,那么这里有机会执行其他函数,但是也得不停的再次调用accept(), recvfrom()来检查数据是否到来
b)如果数据到来,那么就得卡在recvfrom函数这里把数据从内核缓冲区复制到用户缓 冲区,所以复制这个阶段是卡着完成的
12.同步与异步I/O
(1)异步I/O
a)调用一个异步I/O函数时,我门要给这个函数指定一个接收缓冲区,还要给 定 一个回调函数;调用完一个异步I/O函数后,该函数会立即返回。 其余判断(如有 没有数据发过来等等)交给操作系统,操作系统会判断数据是否到来,如果数据到 来 了,操作系统会把数据拷贝到我们所提供的缓冲区里,然后调用我们所指定的这个 回调函数
b)相关图示
(2)同步I/O
a)相关图示
b)调用select()判断有没有数据,有数据,走下来,没数据卡在那里
c)select()返回之后,用recvfrom()去取数据,取数据的时候也会在那里卡着直到把数据 取完
d)同步I/O和阻塞式I/O比,同步I/O就是所谓的I/O复用
14.非阻塞和异步I/O的差别
(1)非阻塞I/O要不停的调用I/O函数来检查数据是否来,如果数据来了,就得卡在I/O函 数这里把数据从内核缓冲区复制到用户缓冲区,然后这个函数才能返回
(2)异步I/O根本不需要不停的调用I/O函数来检查数据是否到来,只需要调用一次,然 后别的判断的工作就交给操作系统内核来做;内核判断有数据到来,就拷贝数据到我 们提供的缓冲区,调用你的回调函数来通知我们
监听端口实战,epoll介绍及原理详析
1.监听端口的关键代码
bool CSocket::ngx_open_listening_ports()
{
bool ret = true;
CConfig* conf = CConfig::GetInstance();
int sockfd;
int port;
struct sockaddr_in serv_sock;
char strinfo[100];
memset(&serv_sock,0,sizeof(struct sockaddr_in));
//设置地址协议族为IPV4
serv_sock.sin_family = AF_INET;
//监听本机上的所有网卡的所有ip地址
serv_sock.sin_addr.s_addr = htonl(INADDR_ANY);
m_listenPortCount = conf->GetIntDefault("ListenPortCount",m_listenPortCount);
for(int i = 0;i < m_listenPortCount;i++)
{
sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
ret = false;
ngx_log_write_into_file(NGX_LOG_ERR,errno,"Call function socket(AF_INET,SOCK_STREAM,0) fail in function ngx_open_listening_ports ,i=%d.",i);
return ret;
}
//设置套接字选项,让TIME_WAIT后仍能监听
int reuseaddr = 1;
if(setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,(const void*)&reuseaddr,sizeof(reuseaddr)) == -1)
{
ret = false;
close(sockfd);
ngx_log_write_into_file(NGX_LOG_ERR,errno,"Call function setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,(const void*)&reuseaddr,sizeof(reuseaddr)) fail in function ngx_open_listening_ports ,i=%d.",i);
return ret;
}
//设置非阻塞
if(setNonBlocking(sockfd) == false)
{
ret = false;
close(sockfd);
ngx_log_write_into_file(NGX_LOG_ERR,errno,"Call function setNonBlocking(sockfd) fail in function ngx_open_listening_ports ,i=%d.",i);
return ret;
}
strinfo[0] = 0;
sprintf(strinfo,"ListenPort%d",i);
port = conf->GetIntDefault(strinfo,1025 + i);
serv_sock.sin_port = htons((in_port_t)port); // in_port_t == uint16_t
//绑定服务器地址结构体
if(bind(sockfd,(struct sockaddr*)&serv_sock,sizeof(serv_sock)) == -1)
{
ret = false;
close(sockfd);
ngx_log_write_into_file(NGX_LOG_ERR,errno,"Call function bind(sockfd,(struct sockaddr*)&serv_sock,sizeof(serv_sock)) fail in function ngx_open_listening_ports ,i=%d.",i);
return ret;
}
//开始监听
if(listen(sockfd,NGX_LISTEN_BACKLOG) == -1)
{
ret = false;
ngx_log_write_into_file(NGX_LOG_ERR,errno,"Call function listen(sockfd,NGX_LISTEN_BACKLOG) fail in function ngx_open_listening_ports ,i=%d.",i);
return ret;
}
//加入到vector
lpngx_listening_t plisten = new ngx_listening_t;
if(plisten != NULL)
{
memset(plisten,0,sizeof(ngx_listening_t));
plisten->port = port;
plisten->sockfd = sockfd;
ngx_log_write_into_file(NGX_LOG_INFO,0,"Listen port %d successfully in function ngx_open_listening_ports ,i=%d.",port,i);
m_listenSocketList.push_back(plisten);
}
else
{
ngx_log_write_into_file(NGX_LOG_ERR,errno,"No memory to create ngx_listening_t object in function ngx_open_listening_ports ,i=%d.",i);
}
}
return ret;
}
void CSocket::ngx_close_listening_ports()
{
for(int i = 0;i < m_listenPortCount;i++)
{
if(close(m_listenSocketList[i]->sockfd) == -1)
{
ngx_log_write_into_file(NGX_LOG_ERR,errno,"Fail to close port %d in function ngx_close_listening_ports,i = %d",m_listenSocketList[i]->port,i);
}
ngx_log_write_into_file(NGX_LOG_INFO,0,"close port %d successfully in function ngx_close_listening_ports,i = %d",m_listenSocketList[i]->port,i);
}
}
2.监听端口分析
(1)相关结构体
a)通用地址结构struct sockaddr
struct sockaddr {
unsigned short sa_family;
char sa_data[14];
};
sa_family是通信类型,最常用的值是 AF_INET表示IPV4
sa_data14字节,包含套接字中的目标地址和端口信息
b)struct sockaddr_in中的in 表示internet,就是网络地址,是我们比较常用的地址结构,属于AF_INET地址族;
sockaddr_in结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量 中
struct sockaddr_in {
short int sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
struct in_addr {
unsigned long s_addr;
};
unsigned char sin_zero[8];
};
注意:
i)sin_port和sin_addr都必须是NBO(网络字节顺序),一般可视化的数字都是 HBO(本机字节顺序)
ii)NBO,HBO二者转换
inet_addr() 将字符串点数格式地址转化成无符号长整型(unsigned long s_addr s_addr;)
inet_aton() 将字符串点数格式地址转化成NBO
inet_ntoa () 将NBO地址转化成字符串点数格式
htonl()–“Host to Network Long” 长整型数据主机字节顺序转网络字节顺序
ntohl()–“Network to Host Long” 长整型数据网络字节顺序转主机字节顺序
htons()–“Host to Network Short” 短整型数据主机字节顺序转网络字节顺序
ntohs()–“Network to Host Short” 短整型数据网络字节顺序转主机字节顺序
常用的是htons(),inet_addr()正好对应结构体的端口类型和地址类型
iii)sockaddr 和 sockaddr_in的相互关系
一般先给sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的 函数,如bind函数
(2)相关函数
a)htonl(INADDR_ANY);
i)解释
h----host 本地主机
to----to
n----net 网络
l----unsigned long
ii)功能:把本机字节顺序转化为网络字节顺序(将参数的存储方式转为大端存储 方式然后返回)
iii)INADRR_ANY表示0.0.0.0,即表示“不确定地址”,或“所有地址”、“任意 地址”
b)sockfd = socket(AF_INET,SOCK_STREAM,0);
i)函数原型
int socket(int domain, int type, int protocol);
ii)解析
①domain参数为底层协议族:AF_INET(用于Internet);对于UNIX本地域协 议族为 AF_UNIX。
②type参数指定服务类型:SOCK_STREAM(流服务,TCP协议),SOCK_DGRAM (数据报,UDP协议)。
③protocol参数是在前两个参数构成的协议集合下,再选择一个具体的协议。 不过这个值通常都是唯一的(前两个值已经完全决定了它的值)。几乎在所 有情况下都设置为0, 表示使用默认协议。
④调用成功返回一个socket文件描述符,失败返回-1,并设置errno
c)setsockopt函数在第十九课第8点已讲
d)bind(sockfd,(struct sockaddr*)&serv_sock,sizeof(serv_sock));
i)int bind(int socket, struct sockaddr* my_addr, socklen_t addrlen);
bind将本地的端口同socket返回的文件描述符捆绑在一起.成功是返回0,失败 的情况和socket一样
e)listen(sockfd,NGX_LISTEN_BACKLOG)
i)int listen(int sockfd, int backlog)
参数sockfd是被listen函数作用的套接字
参数backlog是侦听队列的长度
ii)成功返回0,失败返回-1,然后设置errno
iii)错误信息
EADDRINUSE:另一个socket也在监听同一个端口
EBADF:参数sockfd为非法的文件描述符
ENOTSOCK:参数sockfd不是文件描述符
EOPNOTSUPP:套接字类型不支持listen操作
f)ioctl(sockfd,FIONBIO,&nb) ====> 设置非阻塞
int nb = 1;
if(ioctl(sockfd,FIONBIO,&nb) == -1)
{
return false;
}
i)详解
①本函数影响由sockfd参数引用的一个打开的文件
②第三个参数总是一个指针,但指针的类型依赖于request参数
③我们可以把和网络相关的请求划分为6类:
套接口操作
文件操作
接口操作
ARP高速缓存操作
路由表操作
流系统
④下表列出了网络相关ioctl请求的request参数以及arg地址必须指向的数据 类型:(这里只需了解文件相关内容)
套接口操作
明确用于套接口操作的ioctl请求有三个,它们都要求ioctl的第三个参数是指向某个整数的一个指针。
SIOCATMARK:如果本套接口的的度指针当前位于带外标记,那就通过由第三个参数指向的整数返回一个非0值;否则返回一个0值。POSIX以函数sockatmark替换本请求。
SIOCGPGRP:通过第三个参数指向的整数返回本套接口的进程ID或进程组ID,该ID指定针对本套接口的SIGIO或SIGURG信号的接收进程。本请求和fcntl的F_GETOWN命令等效,POSIX标准化的是fcntl函数。
SIOCSPGRP:把本套接口的进程ID或者进程组ID设置成第三个参数指向的整数,该ID指定针对本套接口的SIGIO或SIGURG信号的接收进程,本请求和fcntl的F_SETOWN命令等效,POSIX标准化的是fcntl操作。
文件操作
以下5个请求都要求ioctl的第三个参数指向一个整数。
FIONBIO:根据ioctl的第三个参数指向一个0或非0值分别清除或设置本套接口的非阻塞标志。本请求和O_NONBLOCK文件状态标志等效,而该标志通过fcntl的F_SETFL命令清除或设置。
FIOASYNC:根据iocl的第三个参数指向一个0值或非0值分别清除或设置针对本套接口的信号驱动异步I/O标志,它决定是否收取针对本套接口的异步I/O信号(SIGIO)。本请求和O_ASYNC文件状态标志等效,而该标志可以通过fcntl的F_SETFL命令清除或设置。
FIONREAD:通过由ioctl的第三个参数指向的整数返回当前在本套接口接收缓冲区中的字节数。本特性同样适用于文件,管道和终端。
FIOSETOWN:对于套接口和SIOCSPGRP等效。
FIOGETOWN:对于套接口和SIOCGPGRP等效。
接口配置
得到系统中所有接口由SIOCGIFCONF请求完成,该请求使用ifconf结构,ifconf又使用ifreq
结构,如下所示:
Struct ifconf{
int ifc_len; // 缓冲区的大小
union{
caddr_t ifcu_buf; // input from user->kernel
struct ifreq *ifcu_req; // return of structures returned
}ifc_ifcu;
};
#define ifc_buf ifc_ifcu.ifcu_buf //buffer address
#define ifc_req ifc_ifcu.ifcu_req //array of structures returned
#define IFNAMSIZ 16
struct ifreq{
char ifr_name[IFNAMSIZ]; // interface name, e.g., “le0”
union{
struct sockaddr ifru_addr;
struct sockaddr ifru_dstaddr;
struct sockaddr ifru_broadaddr;
short ifru_flags;
int ifru_metric;
caddr_t ifru_data;
}ifr_ifru;
};
#define ifr_addr ifr_ifru.ifru_addr // address
#define ifr_dstaddr ifr_ifru.ifru_dstaddr // otner end of p-to-p link
#define ifr_broadaddr ifr_ifru.ifru_broadaddr // broadcast address
#define ifr_flags ifr_ifru.ifru_flags // flags
#define ifr_metric ifr_ifru.ifru_metric // metric
#define ifr_data ifr_ifru.ifru_data // for use by interface
在调用ioctl前我们必须先分配一个缓冲区和一个ifconf结构,然后才初始化后者。如下图展示了一个ifconf结构的初始化结构,其中缓冲区的大小为1024,ioctl的第三个参数指向
这样一个ifconf结构。
假设内核返回2个ifreq结构,ioctl返回时通过同一个ifconf结构缓冲区填入了那2个ifreq结构,ifconf结构的ifc_len成员也被更新,以反映存放在缓冲区中的信息量。
接口操作
SIOCGIFCONF请求为每个已配置的接口返回其名字以及一个套接口地址结构。我们接着可以发出多个接口类其他请求以设置或获取每个接口的其他特征。这些请求的获取(get)版本(SIOCGxxx)通常由netstat程序发出,设置(set)版本(SIGOCSxxx)通常由ifconfig程序发出。任何用户都可以获取接口信息,设置接口信息却要求有超级用户权限。
这些请求汲取或返回一个一个ifreq结构中的信息,而这个结构的地址则作为ioctl调用的第三个参数制定。接口总是以其名标志,在ifreq结构的ifr_name成员中指定,如le0,lo0,ppp0等。
这些请求中有许多使用套接口地址结构在应用进程和内核之间指定或返回具体接口的IP地址或地址掩码。对于IPV4,这个地址或掩码放在一个网际套接口地址结构的sin_addr成员中;对于IPV6,它是一个IPV6套接口地址结构的sin6_addr成员。
SIOCGIFADDR: 在ifr_addr成员中返回单播地址。
SIOCSIFADDR:用ifr_addr成员设置接口地址,这个接口的初始化函数也被调用。
SIOCGIFFLAGS:在ifr_flags成员中返回接口标志。这些接口标志的名字格式为IFF_XXX,在
SIOCSIFFLAGS:用ifr_flags成员设置接口标志。
SIOCGIFDSTADDR:在ifr_dstaddr成员中返回点到点地址。
SIOCSIFDSTADDR: 在ifr_dstaddr成员中设置点到点地址
SIOCGIFBRDADDR: 在ifr_broadaddr成员中返回广播地址。应用进程必须首先获取接口标志,然后发出正确的请求;对于广播接口为SIOCGIFBRDADDR,对于点到点接口为SIOCGIFDSTADDR
SIOCSIFBRDADDR:用ifr_broadaddr成员设置广播地址。
SIOCGIFNETMASK:在ifr_addr成员中返回子网掩码。
SIOCSIFNETMASK:在ifr_addr成员中设置子网掩码。
SIOCGIFMETRIC:用ifr_metric成员返回接口测度。接口测度由内核为每个接口维护,不过使用他的是路由守护进程routed。接口测度被routed加到跳数上。
SIOCSIFMETRIC:用ifr_metric成员设置接口的路由测度。
ARP高速缓存操作
ARP告诉缓存也通过ioctl函数操纵。使用路由域套接口的系统往往改用路由套接口访问ARP高速缓存。这些请求使用如下的arpreq结构,定义在
struct arpreq {
struct sockaddr arp_pa;
struct sockaddr arp_ha;
int arp_flags;
};
#define ATF_INUSE 0x01 //entry in use
#define ATF_COM 0x02 //completed entry (hardware addr valid)
#define ATF_PERM 0x04 // permanent entry
#define ATF_PUBL 0x08 // published entry (respond for other host )
Ioctl的第三个必须指向某个arpreq结构,操纵ARP高速缓存的ioctl请求有以下三个:
①SIOCSARP:把一个新的表项加入ARP告诉缓存中区,或者修改其中已经存在的一个表项,其中arp_pa是一个含有IP地址的网际套接口地址结构,arp_ha则是一个通用套接口地址结构,他的sa_family值为AF_unspec,sa_data中含有硬件地址(例如6直接的以太网地址)。ATF_PERM和ATF_PUBL这两个标志也可以由应用进程指定。另外两个标志(ATF_INUSE和ATF_COM)则由内核设置。
②SIOCDARP:从ARP告诉缓存中删除一个表项。调用者指定要删除表项的网际地址。
③SIOCGARP:从ARP高速缓存中获取一个表项。调用者指定网际地址,相应的硬件地址(例外以太网地址)随标志一起返回。
只有超级用户才能增加或删除表项。这三个请求通常由arp程序发出。
注意ioctl没有办法列出ARP高速缓存中的所有表项。当指定-a标志执行arp命令时,大多数版本的arp程序通过读取内核的内存( /dev/kmem )获得ARP高速缓存的当前内容。
路由表操作
有些系统提供2个用于操纵路由表的ioctl请求。这2个请求要求ioctl的第三个参数是指向某个rtentry结构的一个指针,该结构定义在
SIOCADDRT:往路由表中增加一个表项
SIOCDELRT:从路由表中删除一个表项
Ioctl没有办法列出路由表中的所有表项。这个操作通常由netstat程序在指定-r标志自行四完成。netstat程序通过读取内核的内存 (/dev/kmem)获得整个路由表。用sysctl同样可以做到。
3.epoll概述
(1)epoll就是一种典型的I/O多路复用技术,epoll技术的最大特点是支持高并发
(2)freebsd上有kqueue技术
(3)epoll性能依赖于内存,并发每增加一个,必定要消耗一定的内存去保存这个连接相关 的数据
(4)epoll事件驱动机制,在单独的进程或者单独的线程里运行,收集/处理事件,没有进 程/线程之间切换的消耗,高效
4.相关资料
(1)https://github.com/wangbojing
5.epoll_create()函数
(1)原型:int epoll_create(int size);
(2)功能:创建一个epoll对象,返回该对象的描述符【类似于文件描述符】,这个描述符 就代表这个epoll对象
(3)size就是在该对象的描述符上能关注的最大socket数,要大于零
(4)原理
红黑树中的结点结构:
epoll内部用了红黑树和双向链表的数据结构。红黑树用来保存所有socket,双向链表中保存红黑树中有数据过来的socket。
注意,双向链表中的数据是从红黑树中来的,但往双向链表中插入或者删除结点,并不会影响红黑树中的数据。
6.epoll_ctl()函数
(1)原型:int epoll_ctl(int efpd,int op,int sockid,struct epoll_event* event);
(2)功能:把一个socket以及这个socket相关的事件添加到这个epoll对象描述符中去, 目的就是通过这个epoll对象来监视这个socket【客户端的TCP连接】上数据的来往情 况
(3)我们把感兴趣的事件通过epoll_ctl()添加到系统,当这些事件来的时候,系统会通 知我们
(4)参数说明
a)efpd:epoll_create()返回的epoll对象描述符
b)op:动作,添加/删除/修改 ,对应数字是1,2,3, EPOLL_CTL_ADD, EPOLL_CTL_DEL ,EPOLL_CTL_MOD
i)EPOLL_CTL_ADD添加事件:往红黑树上添加一个节点,每个客户端连入 服务器后,服务器都会产生一个对应的socket,每个连接的socket值都不重复。 所以,这个socket就是红黑树中的key,把这个节点添加到红黑树上去。
ii)EPOLL_CTL_MOD:修改事件,前提是用了EPOLL_CTL_ADD把节点添加到红黑树 上之后,才允许修改
iii)EPOLL_CTL_DEL:从红黑树上把这个节点干掉,这会导致这个socket【这个tcp 链接】上无法收到任何系统通知事件
c)sockid:要监听的socket的描述符,表示客户端连接,这个是红黑树里边的key
d)event:事件信息,这里包括的是一些事件信息;EPOLL_CTL_ADD和EPOLL_CTL_MOD 都要用到这个event参数里边的事件信息
(5)struct epoll_event结构体
a)功能:用于注册所感兴趣的事件和回传所发生待处理的事件
b)结构体定义
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;//保存触发事件的某个文件描述符相关的数据
struct epoll_event {
__uint32_t events; /* epoll event /
epoll_data_t data; / User data variable */
};
其中events表示感兴趣的事件和被触发的事件,可能的取值为:
EPOLLIN: 表示对应的文件描述符可以读;
EPOLLOUT: 表示对应的文件描述符可以写;
EPOLLPRI: 表示对应的文件描述符有紧急的数可读;
EPOLLERR: 表示对应的文件描述符发生错误;
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: epoll的ET工作模式;
7.epoll_wait()函数
(1)原型:int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout);
(2)功能:阻塞一小段时间并等待事件发生,返回事件集合,也就是获取内核的事件通知。 说白了就是遍历双向链表,把这个双向链表里边的节点数据拷贝出去,拷贝完毕的就 从双向链表里移除;因为双向链表里记录的是所有有数据/有事件的socket【TCP连接】
(3)参数说明
a)参数epfd:是epoll_create()返回的epoll对象描述符
b)参数events:是内存,也是数组,长度是maxevents,表示此次epoll_wait调用可以 收集到的maxevents个已经继续【已经准备好的】的读写事件;说白了,就是返回 的是实际发生事件的tcp连接数目
c)参数timeout:阻塞等待的时长
(4)epoll_wait运行的原理:等侍注册在epfd上的sockid的事件的发生,如果发生则将发生的sokct fd和事件类型放入到events数组中。并且将注册在epfd上的sockid的事件类型给清空,所以如果下一个循环我们还要关注这个sockid的话,则需要用epoll_ctl(epfd,EPOLL_CTL_MOD,listenfd,&ev)来重新设置sockid的事件类型。这时不用EPOLL_CTL_ADD,因为socket fd并未清空,只是事件类型清空
8.一般有四种主要情况,会使操作系统把节点插入到双向链表中
(1)客户端完成三路握手时会向双向链表增加节点,这时候服务器应该要accept()
(2)当客户端关闭连接时会向双向链表增加节点,这时服务器应该要调用close()关闭
(3)有客户端发送数据过来时会向双向链表增加节点,这时服务器应该要调用read()或 recv()等函数来收数据
(4)当可以发送数据时会向双向链表增加节点,这时服务武器可以调用send()、write()函数
(5)…
通讯代码精粹之epoll函数实战1
1.类成员函数指针
(1)定义方法
typedef 返回值类型 (类名::*指针变量名)(函数参数)
(2)例子
typedef void (CSocket::ngx_event_handler_pt)(lpngx_connection_t c);
(3)使用方法
c->rhandler = &CSocket::ngx_event_accept;
(this->(c->rhandler))©;
2.连接池
(1)将一个数组的每一个元素连接成链表,方便查找可用的空闲连接结点
(2)相关代码
//连接池数组
m_pconnections = new ngx_connection_s[m_worker_connections];
m_connection_n = m_worker_connections;
//将数组串成链表
int i = m_connection_n;
lpngx_connection_t pnext = NULL;
lpngx_connection_t tmp = m_pconnections;
do
{
i--;
tmp[i].next = pnext;
/*一些初始化操作*/
pnext = &tmp[i];
} while(i);
//指向空闲链表首结点
m_pfree_connections = pnext;
m_free_connection_n = m_connection_n;
3.技巧
(1)代码
struct epoll_event event;
/…/
event.data.ptr = (void*)((uintptr_t)c | c->instance);
(2)解析
a)instance声明为unsigned instance:1;
这是位域操作,即instance成员只占1位
b)一个前提知识点:malloc得到的地址转换成二进制后的最后一位肯定是0
c)通过上面的位运算,可以将c所指向的地址和instance的值都保存到ptr中,后面也 可以通过位运算将它们分离出来
d)分离方法
c = (lpngx_connection_t)(m_events[i].data.ptr);
instance = (uintptr_t)c & 1;
c = (lpngx_connection_t)((uintptr_t)c & (uintptr_t)~1);
4.列出哪些进程在监听80端口
(1)方法一:lsof -i:80
(2)方法二:netstat -tunlp | grep 80
5.执行流程
(1)通过调用epoll_create()函数来获得一个epoll对象句柄
(2)new一个数组,并串成链表来做连接池
(3)在for循环中,循环次数为监听端口的数目,每次循环都从连接池的空闲链表中拿出一个空闲结点跟m_listenSocketList容器中的第i个元素,即保存了监听端口信息的lpngx_listening_t指针互相关联,方便以后互相调用
(4)设置监听端口的读事件的处理方法
(5)调用epoll_ctl()函数往epoll对象中添加事件
通讯代码精粹之epoll函数实战2
1.accept函数详解
(1)函数原型
int accept(int sockfd,struct sockaddr* addr,socklen_t* addrlen);
(2)功能
accept()系统调用主要用在基于连接的套接字类型,比如SOCK_STREAM和SOCK_SEQPACKET。它提取出所监听套接字的已完成连接队列中第一个连接请求,创建一个新的套接字,并返回指向该套接字的文件描述符。新建立的套接字不在监听状态,原来所监听的套接字也不受该系统调用的影响。
(3)参数说明
sockfd:利用系统调用socket()建立的套接字描述符,通过bind()绑定到一个本地地址(一般为服务器的套接字),并且通过listen()一直在监听连接;
addr:指向struct sockaddr的指针,该结构用通讯层服务器对等套接字的地址(一般为客户端地址)填写,返回地址addr的确切格式由套接字的地址类别(比如TCP或UDP)决定;若addr为NULL,没有有效地址填写,这种情况下,addrlen也不使用,应该置为NULL;
addrlen:一个值结果参数,调用accept函数前必须初始化addrlen为包含addr所指向结构大小的数值(sizeof(struct sockaddr)),函数返回时包含对等地址的实际数值
(4)可能的错误
a)EAGAIN 或 EWOULDBLOCK:套接口被标记为非阻塞并且没有连接等待接受。 POSIX.1-2001允许在此时返回这两种错误,但没有要求两个常量必须具有相同的值,所 以可移植的程序应该同时检查两者。
b)EBADF:描述符无效。
c)ECONNABORTED:一个连接已经中止了。
d)EFAULT:参数 addr 不在可写的用户地址空间里。
e)EINTR:在一个有效的连接到达之前,本系统调用被信号中断,参看 signal(7)。
f)EINVAL:套接口不在监听连接,或 addrlen 无效(如是负数)。
g)EINVAL:(accept4()) 在 flags 中有无效的值。
h)EMFILE:达到单个进程打开的文件描述上限。
i)ENFILE:达到系统允许打开文件个数的全局上限。
j)ENOBUFS, ENOMEM:没有足够的自由内存。这通常是指套接口内存分配被限制,而 不是指系统内存不足。
k)ENOTSOCK:描述符是一个文件,不是一个套接字。
l)EOPNOTSUPP:引用的套接口不是 SOCK_STREAM 类型的。
m)EPROTO:协议错误。
n)EPERM:防火墙规则禁止连接。
2.accept4()函数
(1)函数原型
int accept4(int sockfd, struct sockaddr *addr,socklen_t *addrlen, int flags);
(2)相对于accept函数的区别在于flags参数,可选值有
SOCK_NONBLOCK:在新打开的文件描述符设置 O_NONBLOCK 标记,让函数返回得到的 socket设置为非阻塞,在此项目中就选这个标记
SOCK_CLOEXEC:在新打开的文件描述符里设置 close-on-exec (FD_CLOEXEC) 标记。意思 是:子进程默认是继承父进程打开的所有fd,当调用exec()函数成 功后,文件描述符会自动关闭。(很少用)
3.用户三次握手成功连入进来,这个“连入进来”这个事件对于服务器来讲,就是一个监听套接字上的可读事件
4.epoll的两种工作模式
(1)LT:level trigged,水平触发,这种工作模式为低速模式(效率差),这是epoll缺省使 用的模式
a)一个事件到来,若不把这个事件完全处理完毕(如收到数据但是没有接收完)的话, 会多次调用事件处理函数
(2)ET:edge trigged,边缘触发/边沿触发,这种工作模式为高速模式(效率好)
a)一个事件到来,不管有没有完全处理掉或者没有处理那个事件,那么内核只会通知我们一次,即那个事件处理函数只会被调用一次
b)ET模式只对非阻塞socket有用
5.准备好各种工作之后,编写int CSocket::ngx_epoll_process_events(int timer) 函数,里面调用epoll_wait()函数来尝试获取事件。如果获取到事件,那么判断事件的类型(读、写等等)来执行相应动作。ngx_epoll_process_events函数在worker子进程的事件循环中不断被调用。
6.一道腾讯后台开发的面试题
(1)问题:使用Linux epoll模型,水平触发模式;当socket可写时,会不停的触发socket 可写的事件,如何处理?
(2)解答:
a)第一种最普遍的方式:
需要向socket写数据的时候才把socket加入epoll【红黑树】,等待可写事件。接 受到可写事件后,调用write或者send发送数据。当所有数据都写完后,把socket移 出epoll。
这种方式的缺点是,即使发送很少的数据,也要把socket加入epoll,写完后在移 出epoll,有一定操作代价。
b)一种改进的方式:
开始不把socket加入epoll,需要向socket写数据的时候,直接调用write或者send 发送数据。如果返回EAGAIN,把socket加入epoll,在epoll的驱动下写数据,全部数 据发送完毕后,再移出epoll。
这种方式的优点是:数据不多的时候可以避免epoll的事件处理,提高效率。
7.struct epoll_event中的events成员可选的事件值
(1)EPOLLIN
a)当监听端口的socket中有新连接请求,触发EPOLLIN
b)对端发送数据过来,当前有数据可读,触发EPOLLIN
c)对端正常关闭(在程序里close()即正常四次挥手断开连接,shell下kill或crt+c), 触发EPOLLIN和EPOLLRDHUP
d)调用recv返回0,表示连接正常关闭,触发EPOLLIN
(2)EPOLLOUT
a)socket的发送缓冲区未满,可以写数据时,触发EPOLLOUT
(3)EPOLLRDHUP (有些系统不支持,则使用EPOLLIN)
a)调用read返回0,触发EPOLLRDHUP
b)删除掉事件,触发EPOLLRDHUP
c)调用close(fd),触发EPOLLRDHUP
(4)EPOLLPRI
a)对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来),触发 EPOLLPRI
(5)EPOLLERR
a)对应的文件描述符发生错误,触发EPOLLERR
(6)EPOLLHUP
a)对应的文件描述符被挂断,触发EPOLLHUP
(7)EPOLLONESHOT
a)只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里
(8)EPOLLET
a)将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered) 来说的
ET,LT深释,服务器设计,粘包解决
1.为什么ET模式事件只触发一次?
答:事件被扔到双向链表中一次,被epoll_wait取出后就从双向链表都删除该事件结点
2.为什么LT模式事件会触发多次呢?
答:事件如果没有处理完,那么事件会被多次加入到双向链表中
3.ET和LT的选择
(1)如果收发数据包有固定格式,那么建议采取LT
(2)如果收发数据包没有固定格式,可以考虑采用ET模式
(3)ET模式的代码编写难度大很多
4.粘包问题
(1)不管是客户端还是服务端,都会出现粘包现象。但是只需要处理服务端的粘包问题
(2)当调用send或者write函数发送数据时,调用完后不一定就已经把数据发送出去,可 能会因为数据太少,操作系统会等待数据多一点的时候才一下子全部发出去
(3)当要send或者write的数据太大的时候,操作系统可能会把该数据拆分成几个数据包 再发送出去
5.粘包解决
(1)在客户端和服务端制订一个收发包的协议(制定包头、包体等格式),即不管是发送数据包还是接收数据包,都用统一的格式
通讯代码精粹之收包解包实战
1.将收包分成四个状态
#define _PKG_HD_INIT 0 //初始状态,准备接收数据包头
#define _PKG_HD_RECVING 1 //接收包头中,包头不完整,继续接收中
#define _PKG_BD_INIT 2 //包头刚好收完,准备接收包体
#define _PKG_BD_RECVING 3 //接收包体中,包体不完整,继续接收中,处理后直接回到_PKG_HD_INIT状态
2.服务器收包时,当接收完包头后,申请一段内存,这时候额外附加一个消息头(用来检验包是否过期)放在整个收包缓冲区的最前面,即收到一个完整的包后,整体结构为【消息头 + 包头 + 包体】
3.注意事项
(1)包头结构体要指定按1字节对齐,在客户端和服务器端都要一致按1字节对齐,防止环境不同时sizeof(包头结构体)的大小不一致,这样收发包会错乱
(2)只是包头结构体需要按1字节对齐,其他结构不需要
(3)相关代码
#pragma pack (1)
typedef struct pkg_header_t
{
unsigned short pkgLen; //报文总长度【包头+包体】
unsigned short msgCode; //消息类型代码–2字节,用于区别每个不同的命令【不同 的消息】
int crc32; //CRC32效验–4字节,为了防止收发数据中出现收到内容和 发送内容不一致的情况,引入这个字段做一个基本的校验用
}pkg_header_t,*lppkg_header_t;
#pragma pack() //取消指定对齐,恢复缺省对齐
4.在服务器收包之后,从包头结构体变量中获取报文总长度时,需要将将网络字节流(大端)转换为本机的short类型
(1)代码
unsigned short pklen = ntohs(ph->pkgLen);
5.recv函数
(1)函数原型
int recv( SOCKET s, char FAR *buf, int len, int flags );
(2)参数说明
a)第一个参数指定接收端套接字描述符;
b)第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
c)第三个参数指明buf的长度;
d)第四个参数一般置0
(3)执行流程
a)recv先等待 SOCKET s 的发送缓冲中的数据被协议传送完毕,如果协议在传送s的 发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR;
b)如果s的发送缓冲区中没有数据或者数据被协议成功发送完毕后,recv先检查套接 字s的接收缓冲区;
c)如果s的接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直 到协议把数据接收完毕;
d)当协议把数据接收完毕,recv函数就把s的接收缓冲区中的数据copy到buf中。(注 意协议接收到的数据可能大于buf的长度,所以 在这种情况下要调用几次recv函 数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数 据是协议来完成的), recv函数返回其实际copy的字节数。如果recv在copy时出 错,那么它返回SOCKET_ERROR;
e)如果recv函数在等待协议接收数据时网络中断了,那么它返回0
(4)返回值
a)成功执行时,返回接收到的字节数。
b)另一端已关闭(正常四次挥手关闭)则返回0
c)失败返回-1,errno被设为以下的某个值 :
EAGAIN:套接字已标记为非阻塞,而接收操作被阻塞或者接收超时
EBADF:sock不是有效的描述词
ECONNREFUSE:远程主机阻绝网络连接
EFAULT:内存空间访问出错
EINTR:操作被信号中断
EINVAL:参数无效
ENOMEM:内存不足
ENOTCONN:与面向连接关联的套接字尚未被连接上
ENOTSOCK:sock索引的不是套接字 当返回值是0时,为正常关闭连接
特别:返回值<0时并且(errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的 情况下认为连接是正常的,继续接收
EWOULDBLOCK:用于非阻塞模式,不需要重新读或者写
EINTR:指操作被中断唤醒,需要重新读/写
如果出现EINTR即errno为4,错误描述Interrupted system call,操作应该继续。
EAGAIN:Linux - 非阻塞socket编程处理EAGAIN错误
在linux进行非阻塞的socket接收数据时经常出现Resource temporarily unavailable,errno代码为11(EAGAIN)。从字面上来看,是提示再试一次。这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。这个错误不会破坏socket的同步,不用管它,下次循环接着recv就可以
6.send函数
(1)函数原型
int send( SOCKET s, const char FAR *buf, int len, int flags );
(2)参数说明
a)第一个参数指定发送端套接字描述符;
b)第二个参数指明一个存放应用程序要发送数据的缓冲区;
c)第三个参数指明实际要发送的数据的字节数;
d)第四个参数一般置0
(3)关于flags参数
MSG_CONFIRM :用来告诉链路层
MSG_DONTROUTE:不要使用网关来发送数据,只发送到直接连接的主机上。通常只有 诊断或者路由程序会使用,这只针对路由的协议族定义的,数据包的 套接字没有
MSG_DONTWAIT :启用非阻塞操作,如果操作阻塞,就返回EAGAIN或EWOULDBLOCK
MSG_EOR :当支持SOCK_SEQPACKET时,终止记录
MSG_MORE :调用方有更多的数据要发送。这个标志与TCP或者udp套接字一起使用
MSG_NOSIGNAL :当另一端中断连接时,请求不向流定向套接字上的错误发送SIGPIPE , EPIPE 错误仍然返回
MSG_OOB:在支持此概念的套接字上发送带外数据(例如,SOCK_STREAM类型);底 层协议还必须支持带外数据
(4)send和write的唯一区别就是最后一个参数:flags的存在,当我们设置flags为0时, send和wirte是同等的
8.用Qt写客户端测试程序时,用到htons等相关函数的时候,Qt的配置文件(Qt的Makefile文件)里要加上
LIBS += -lpthread libwsock32 libws2_32
业务逻辑之多线程,线程池实战
1.Makefile时的链接
(1)使用C++11的内容时链接时加上-std=c++11
(2)要使用POSIX标准库的多线程接口,保护头文件
2.互斥锁创建
(1)静态方式
a)POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
在LinuxThreads实现中,pthread_mutex_t是一个结构,而PTHREAD_MUTEX_INITIALIZER则是一个结构常量
(2)动态方式
a)函数
int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* mutexattr)
其中mutexattr用于指定互斥锁属性(见下),如果为NULL则使用缺省属性。
b)互斥锁属性
互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。当前(glibc2.2.3,linuxthreads0.9)有四个值可供选择:
(i)PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
(ii)PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
(iii) PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
(IV)PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。
3.互斥条件创建
(1)静态方式
a)可以用宏PTHREAD_COND_INITIALIZER来初始化静态定义的条件变量,使其具有缺省 属性。这和用pthread_cond_init函数动态分配的效果是一样的。初始化时不进行错 误检查。
pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
(2)动态方式
a)函数
int pthread_cond_init(pthread_cond_t* cv,const pthread_condattr_t* cattr);
返回值:函数成功返回0,任何其他返回值都表示错误
初始化一个条件变量。当参数cattr为空指针时,函数创建的是一个缺省的条件变量。否则条件变量的属性将由cattr中的属性值来决定。调用 pthread_cond_init函数时,参数cattr为空指针等价于cattr中的属性为缺省属性,只是前者不需要cattr所占用的内存开销。这个函数返回时,条件变量被存放在参数cv指向的内存中
4.创建线程
(1)函数
int pthread_create(pthread_t* thread, pthread_attr_t* attr, void* (*start_routine)(void *), void * arg);
a)若线程创建成功,则返回0;若线程创建失败,则返回出错编号
b)参数说明
第一个参数为指向线程标识符的指针。
第二个参数用来设置线程属性,一般为NULL。
第三个参数是线程运行函数的起始地址。
最后一个参数是运行函数的参数。
c)第二个参数可用的值
(i)__detachstate, 表示新线程是否与进程中其他线程脱离同步,如果置位则新线程不能用pthread_join()来同步,且在退出时自行释放所占用的资源。缺省为 PTHREAD_CREATE_JOINABLE状态。这个属性也可以在线程创建并运行以后用pthread_detach()来设置,而一旦设置为 PTHREAD_CREATE_DETACH状态(不论是创建时设置还是运行时设置)则不能再恢复到 PTHREAD_CREATE_JOINABLE状态。
(ii)__schedpolicy,表示新线程的调度策略,主要包括 SCHED_OTHER(正常、非实时)、SCHED_RR(实时、轮转法)和 SCHED_FIFO(实时、先入先出)三种,缺省为SCHED_OTHER,后两种调度策略仅对超级用户有效。运行时可以用过 pthread_setschedparam()来改变。
(iii)__schedparam,一个struct sched_param结构,目前仅有一个sched_priority整型变量表示线程的运行优先级。这个参数仅当调度策略为实时(即SCHED_RR 或SCHED_FIFO)时才有效,并可以在运行时通过pthread_setschedparam()函数来改变,缺省为0。
(IV)__inheritsched, 有两种值可供选择:PTHREAD_EXPLICIT_SCHED和PTHREAD_INHERIT_SCHED,前者表示新线程使用显式指定调度策略和 调度参数(即attr中的值),而后者表示继承调用者线程的值。缺省为PTHREAD_EXPLICIT_SCHED。
(V)__scope, 表示线程间竞争CPU的范围,也就是说线程优先级的有效范围。POSIX的标准中定义了两个值: PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS,前者表示与系统中所有线程一起竞争CPU时间,后者表示仅与同 进程中的线程竞争CPU。目前LinuxThreads仅实现了PTHREAD_SCOPE_SYSTEM一值。
5.互斥量加锁
(1)int pthread_mutex_lock(pthread_mutex_t* mutex);
(2)成功则返回0,失败则返回其他值,但不修改errno的值
6.互斥量解锁
(1)int pthread_mutex_unlock(pthread_mutex_t* mutex);
(2)成功则返回0,失败则返回其他值,但不修改errno的值
7.阻塞线程
(1)int pthread_cond_wait(pthread_cond_t* cond,pthread_mutex_t* mutex);
8.唤醒线程
(1)唤醒一个等待cond条件变量的线程
int pthread_cond_signal(pthread_cond_t* cond);
成功则返回0
(2)唤醒所有等待cond条件变量的线程
int pthread_cond_broadcast(pthread_cond_t* cond);
成功则返回0
9.阻塞等待线程退出
(1)int pthread_join(pthread_t thread, void** retval);
成功:0;失败:errno
(2)参数
thread:线程ID (注意:不是指针,是pthread_create函数的第一个参数值解引用)
retval:存储线程结束状态
(3)thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如 下:
a)如果thread线程通过return返回,retval所指向的单元里存放的是thread线程 函数的返回值(即void *指针);
b)如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的 单元里存放的是常数PTHREAD_CANCELED。
c)如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是 传给pthread_exit的参数(即void *指针),其实1与3是等效的。
d)如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。
10.销毁互斥量
(1)int pthread_mutex_destroy(pthread_mutex_t* mutex);
成功则返回0
11.销毁条件变量
(1)int pthread_cond_destroy(pthread_cond_t* cond);
成功则返回0
12.避免因线程池无可用线程而频繁写日志
(1)可以设定时间间隔,代码如下
if(m_RunningThreadCount == m_threadCount)
{
time_t cur = time(NULL);
//单位为秒,间隔大于10秒才写日志,避免频繁
if((cur - m_LastEmgTime) > 10)
{
m_LastEmgTime = cur;
//写日志
}
}
13.当前项目中的线程池代码
<========== ngx_threadpool.h ==========>
#ifndef _NGX_THREADPOOL_H_
#define _NGX_THREADPOOL_H_
#include
#include
#include
class ThreadPool
{
protected:
struct ThreadItem
{
pthread_t threadHandle;
ThreadPool* pThis;
bool ifRunning;
ThreadItem(ThreadPool* p):pThis(p),ifRunning(false){}
~ThreadItem(){}
};
static pthread_mutex_t m_pthreadMutex; //线程同步互斥量
static pthread_cond_t m_pthreadCond; //线程同步条件变量
static bool m_shutdown; //是否要关闭程序
int m_threadCount; //线程池里的线程数量
std::atomic<int> m_RunningThreadCount; //在运行的线程数量
time_t m_LastEmgTime; //上次发生线程不够用的时间,用来做防止频繁写日志的工作
std::vector<ThreadItem*> m_threadVector; //线程容器
protected:
static void* ThreadFunc(void* threadData); //线程入口函数
void clearVector();
public:
ThreadPool();
bool create(int threadCount);
void stopAll();
void call(int irmqc);
~ThreadPool();
};
#endif
<========== ngx_threadpool.cpp ==========>
#include //usleep
#include "ngx_threadpool.h"
#include "ngx_memory.h"
#include "ngx_log.h"
#include "ngx_global.h"
pthread_mutex_t ThreadPool::m_pthreadMutex = PTHREAD_MUTEX_INITIALIZER; //#define PTHREAD_MUTEX_INITIALIZER ((pthread_mutex_t) -1)
pthread_cond_t ThreadPool::m_pthreadCond = PTHREAD_COND_INITIALIZER; //#define PTHREAD_COND_INITIALIZER ((pthread_cond_t) -1)
bool ThreadPool::m_shutdown = false;
ThreadPool::ThreadPool()
{
m_threadCount = 0;
m_RunningThreadCount = 0;
m_LastEmgTime = 0;
}
void ThreadPool::clearVector()
{
std::vector<ThreadItem*>::iterator it;
for(it = m_threadVector.begin();it != m_threadVector.end();it++)
{
CMemory::GetInstance()->FreeMemory((*it));
it = m_threadVector.erase(it);
}
}
bool ThreadPool::create(int threadCount)
{
ThreadItem* pthreadItem = NULL;
int err = -1;
for(int i = 0;i < threadCount;i++)
{
pthreadItem = new ThreadItem(this);
if(pthreadItem != NULL)
{
m_threadVector.push_back(pthreadItem);
err = pthread_create(&pthreadItem->threadHandle,NULL,ThreadFunc,pthreadItem);
if(err != 0)
{
ngx_log_write_into_file(NGX_LOG_ERR,0,"Fail to call pthread_create() in ThreadPool::create()");
clearVector();
return false;
}
else
{
//线程创建成功
}
}
else
{
ngx_log_write_into_file(NGX_LOG_ERR,0,"No enough memory to create ThreadItem obj");
clearVector();
return false;
}
}
//为保证所有线程都完全创建好
std::vector<ThreadItem*>::iterator it;
lblfor:
for(it = m_threadVector.begin();it != m_threadVector.end();it++)
{
if((*it)->ifRunning == false)
{
usleep(100 * 1000); //单位是微秒,休眠100毫秒
goto lblfor;
}
}
return true;
}
void* ThreadPool::ThreadFunc(void* threadData)
{
ThreadItem* pItem = static_cast<ThreadItem*>(threadData);
ThreadPool* pPool = pItem->pThis;
char* msg = NULL;
int err = -1;
while(true)
{
err = pthread_mutex_lock(&m_pthreadMutex);
if(err != 0)
{
ngx_log_write_into_file(NGX_LOG_ERR,0,"Fail to call pthread_mutex_lock(&m_pthreadMutex) in ThreadPool::ThreadFunc(void* threadData)");
}
while(((msg = g_socket.outMsgRecvQueue()) == NULL) && (m_shutdown == false))
{
if(pItem->ifRunning == false)
{
pItem->ifRunning = true;
}
pthread_cond_wait(&m_pthreadCond,&m_pthreadMutex);
}
//走到这里表示拿到锁
err = pthread_mutex_unlock(&m_pthreadMutex);
if(err != 0)
{
ngx_log_write_into_file(NGX_LOG_ERR,0,"Fail to call pthread_mutex_unlock(&m_pthreadMutex) in ThreadPool::ThreadFunc(void* threadData)");
}
if(m_shutdown)
{
if(msg != NULL)
{
CMemory::GetInstance()->FreeMemory(msg);
break;
}
}
++pPool->m_RunningThreadCount;
//开始 处理业务逻辑
//测试
ngx_log_show_on_stderr(0,"开始执行业务逻辑");
sleep(3);
ngx_log_show_on_stderr(0,"结束执行业务逻辑");
//结束 处理业务逻辑
CMemory::GetInstance()->FreeMemory(msg);
--pPool->m_RunningThreadCount;
}
}
void ThreadPool::stopAll()
{
if(m_shutdown == true)
{
return;
}
m_shutdown = true;
//广播所有在等待m_pthreadCond条件变量激活的线程
int err = pthread_cond_broadcast(&m_pthreadCond);
if(err != 0)
{
ngx_log_write_into_file(NGX_LOG_ERR,0,"Fail to call pthread_cond_broadcast(&m_pthreadCond) in ThreadPool::stopAll");
}
//等待所有线程都执行完毕
std::vector<ThreadItem*>::iterator it;
for(it = m_threadVector.begin();it != m_threadVector.end();it++)
{
pthread_join((*it)->threadHandle,NULL); //等待一个线程终止
}
pthread_mutex_destroy(&m_pthreadMutex);
pthread_cond_destroy(&m_pthreadCond);
clearVector();
ngx_log_write_into_file(NGX_LOG_INFO,0,"Success to stop all thread");
}
void ThreadPool::call(int irmqc)
{
int err = pthread_cond_signal(&m_pthreadCond);
if(err != 0)
{
ngx_log_write_into_file(NGX_LOG_ERR,0,"Fail to call pthread_cond_signal(&m_pthreadCond) in ThreadPool::call");
}
//线程池中无可用线程
if(m_RunningThreadCount == m_threadCount)
{
time_t cur = time(NULL);
//单位为秒,间隔大于10秒才写日志,避免频繁
if((cur - m_LastEmgTime) > 10)
{
m_LastEmgTime = cur;
ngx_log_write_into_file(NGX_LOG_INFO,0,"No availabel thread at this moment");
}
}
}
ThreadPool::~ThreadPool()
{
stopAll();
clearVector();
}
业务逻辑之打通业务处理脉搏实战
1.包体完整性校验
(1)通过CRC32校验算法,客户端在发送数据包前,根据算法来计算出包体的CRC32值放 在包头中一起发送到服务器。服务器收到完整的数据包后,用同样的CRC32算法计算 出收到的包体的CRC32值。这样通过比较两个值即可知道包体内容有没有被改变。
(2)CRC32类代码
<========== ngx_crc32.h ==========>
#ifndef _NGX_CRC32_H_
#define _NGX_CRC32_H_
#include //NULL
class CCRC32
{
private:
CCRC32();
public:
~CCRC32();
private:
static CCRC32 *m_instance;
public:
static CCRC32* GetInstance()
{
if(m_instance == NULL)
{
//锁
if(m_instance == NULL)
{
m_instance = new CCRC32();
static CGarhuishou cl;
}
//放锁
}
return m_instance;
}
class CGarhuishou
{
public:
~CGarhuishou()
{
if (CCRC32::m_instance)
{
delete CCRC32::m_instance;
CCRC32::m_instance = NULL;
}
}
};
public:
void Init_CRC32_Table();
unsigned int Reflect(unsigned int ref, char ch); // Reflects CRC bits in the lookup table
int Get_CRC(unsigned char* buffer, unsigned int dwSize);
public:
unsigned int crc32_table[256]; // Lookup table arrays
};
#endif
<========== ngx_crc32.cpp ==========>
#include
#include
#include
#include "ngx_crc32.h"
CCRC32 *CCRC32::m_instance = NULL;
CCRC32::CCRC32()
{
Init_CRC32_Table();
}
CCRC32::~CCRC32()
{
}
//初始化crc32表辅助函数
unsigned int CCRC32::Reflect(unsigned int ref, char ch)
{
unsigned int value(0);
for(int i = 1; i < (ch + 1); i++)
{
if(ref & 1)
value |= 1 << (ch - i);
ref >>= 1;
}
return value;
}
//初始化crc32表
void CCRC32::Init_CRC32_Table()
{
unsigned int ulPolynomial = 0x04c11db7;
for(int i = 0; i <= 0xFF; i++)
{
crc32_table[i]=Reflect(i, 8) << 24;
for (int j = 0; j < 8; j++)
{
crc32_table[i] = (crc32_table[i] << 1) ^ (crc32_table[i] & (1 << 31) ? ulPolynomial : 0);
}
crc32_table[i] = Reflect(crc32_table[i], 32);
}
}
//用crc32_table寻找表来产生数据的CRC值
int CCRC32::Get_CRC(unsigned char* buffer, unsigned int dwSize)
{
unsigned int crc(0xffffffff);
int len;
len = dwSize;
while(len--)
crc = (crc >> 8) ^ crc32_table[(crc & 0xFF) ^ *buffer++];
return crc^0xffffffff;
}
2.引入新的CSocket的子类:SLogic
(1)以后用的时候只用CSocket的子类
(2)成员函数threadRecvProcFunc()用来将收到的消息进行校验与分割,在 threadRecvProcFunc()里调用相应的业务处理函数
3.消息的具体处理设计流程
(1)在服务器端设计好业务的分类,业务处理函数封装成SLogic类的成员函数指针,统一 函数参数列表
(2)将不同成员函数指针组合在一个数组里,客户端在发送数据包过来的时候,包头中的 msgCode表示业务类型,直接与数组的下标对应
(3)搭建好框架后,以后服务器的开发工作就可以主要集中在业务逻辑代码的编写中
(4)相关示例代码
if (buf != NULL)
{
//消息头
lpmsg_header_t pMsgHead = reinterpret_cast<lpmsg_header_t>(buf);
if (pMsgHead == NULL)
{
ngx_log_write_into_file(NGX_LOG_ERR, 0, "File: %s Line: %d ==> Fail to reinterpret_cast(buf)" , __FILE__, __LINE__);
return;
}
//连接池中的连接
lpngx_connection_t pConn = pMsgHead->pConn;
//包头
lppkg_header_t pPkgHead = reinterpret_cast<lppkg_header_t>(buf + m_LenMsgHeader);
//包体指针
char *pkgBody = NULL;
if (pPkgHead == NULL)
{
ngx_log_write_into_file(NGX_LOG_ERR, 0, "File: %s Line: %d ==> Fail to reinterpret_cast(buf + m_LenMsgHeader + m_LenPkgHeader)" , __FILE__, __LINE__);
return;
}
int pkgLen = ntohs(pPkgHead->pkgLen);
//只有包头,没有包体
if (pkgLen == m_LenPkgHeader)
{
//规定:若只有包头而没有包体,那么crc32值要为0
if (ntohs(pPkgHead->crc32) != 0)
{
ngx_log_write_into_file(NGX_LOG_ERR, 0, "File: %s Line: %d ==> Package is empty but pPkgHead->crc32 != 0", __FILE__, __LINE__);
return;
}
}
pkgBody = buf + m_LenMsgHeader + m_LenPkgHeader;
CCRC32 *crc = CCRC32::GetInstance();
int crc1 = ntohl(pPkgHead->crc32);
int crc2 = crc->Get_CRC((unsigned char *)pkgBody, (unsigned int)(pkgLen - m_LenPkgHeader));
if (crc1 != crc2)
{
ngx_log_write_into_file(NGX_LOG_ERR, 0, "File: %s Line: %d ==> Package had been changed", __FILE__, __LINE__);
return;
}
//数据包是否过期
if (pConn->iCurrsequence != pMsgHead->iCurrsequence)
{
return;
}
unsigned short msgCode = ntohs(pPkgHead->msgCode);
//非法msgCode
if (msgCode >= HANDLERCOUNT)
{
ngx_log_write_into_file(NGX_LOG_ERR, 0, "File: %s Line: %d ==> pPkgHead->msgCode >= HANDLERCOUNT", __FILE__, __LINE__);
return;
}
if (handlerStatus[msgCode] == NULL)
{
ngx_log_write_into_file(NGX_LOG_ERR, 0, "File: %s Line: %d ==> handlerStatus[msgCode] == NULL", __FILE__, __LINE__);
return;
}
//处理具体的业务
(this->*handlerStatus[msgCode])(pConn, pMsgHead, pkgBody, pkgLen - m_LenMsgHeader);
}
}
4.开发过程中协议的指定
(1)服务器开发人员要主动设计好相关数据包的收发等协议
(2)要跟客户端开发人员商量好,把细节交代清楚,使用一致的格式
预发包,多线程资源回收深度思考
1.本项目架构采用连接池来管理连到服务器的连接。当一个连接建立好之后,断开连接时,刚刚回收的连接池结点会立刻被新连入的客户使用。若处理上一个客户的逻辑线程还没执行完,则可能会出错,造成服务器不稳定
2.延迟回收策略
(1)当连接池中的一个连接断开后,不立即回收该连接池结点,而是等待一定的时间后再回收,以保证处理业务逻辑的线程退出
(2)若连接池满了,则新new出来一个连接池结点,以保证用户能连入
(3)专门用一个线程来不断检测延迟回收队列中是否有要延迟回收的结点,并且时间间隔到了的时候,进行回收操作
3.细节部分看项目代码
LT发数据机制深释,gdb调试浅谈
1.socket发送和接收数据
(1)每一个TCP连接(socket),都会有一个接收缓冲区和一个发送缓冲区,可用setsocketopt()函数来设置缓冲区大小
(2)调用send()或write()发送数据时,实际上这两个函数是把数据放到了发送缓冲区,之后这两个函数返回了
(3)如果服务器端的发送缓冲区满了,那么服务器再调用send()或write()发送数据的时候,那么send()或write()函数就会返回一个EAGAIN
(4)EAGAIN不是一个错误,只是示意发送缓冲区已经满了
(5)当发送缓冲区已经满了的时候,客户端应该调用recv()或read()函数来读取数据。事实上,客户端应该在有数据可读的时候就要调用recv()或read()函数来读取数据,这样服务器端的socket的发送缓冲区才有地方空出来,才能调用send()或write()函数继续发送数据
(6)LT模式下,当socket可写的时候(发送缓冲区没满),会不停的触发socket可写事件
2.gdb调试浅谈
(1)gdb缺省调试主进程,但是gdb 7.0以上版本可以调试子进程
(2)为了让gdb支持多进程调试,要设置一下follow-fork-mode选项,取值可以是parent[主进程] /child[子进程]
a)查看follow-fork-mode:在gdb下输入show follow-fork-mode
b)输入 set follow-fork-mode child
(3)选项 detach-on-fork, 取值为 on/off,默认是on(表示只调试父进程或者子进程其中的一个)。调试是父进程还是子进程,由 follow-fork-mode选项说了算。如果detach-on-fork 的值为 off,就表示父子都可以调试,调试一个进程时,另外一个进程会被暂停
a)查看:show detach-on-fork
b)设置值:输入set show detach-on-fork 值
3.一个问题:当socket可写的时候(发送缓冲区没满),会不停的触发socket可写事件,我们提出两种解决方案
(1)需要向socket写数据的时候把socket写事件通知加入到epoll中,等待可写事件,当可写事件来时操作系统会通知我们;此时我们可以调用wirte/send函数发送数据,当发送数据完毕后,把socket的写事件通知从红黑树中移除
a)缺点:即使发送很少的数据,也需要把事件通知加入到epoll,数据写完毕后,又需 要把写事件通知从红黑树删除,对效率有一定的影响
(2)开始不把socket写事件通知加入到epoll,当需要写数据的时候,直接调用write/send发送数据。如果返回了EAGIN(发送缓冲区满了,需要等待可写事件才能继续往缓冲区里写数据),此时,再把写事件通知加入到epoll。此时,就变成了在epoll驱动下写数据,全部数据发送完毕后,再把写事件通知从epoll中删除
a)优点:数据不多的时候,可以避免epoll的写事件的增加/删除,提高了程序的执行 效率
发数据,信号量,并发,多线程综合实战
1.信号量
(1)头文件 #include
(2)信号量初始化
a)int sem_init(sem_t* sem, int pshared, unsigned int value);
b)pshared 控制信号量的类型,值为 0 指定了 sem 处于共享内存区域,代表该信号 量用于多线程间的同步,值如果大于 0 表示可以共享,用于多个相关进程间的同步
c)value为信号量初始值
(3)信号量阻塞
a)方式一:int sem_wait(sem_t* sem);
i)该函数测试所指定信号量的值,它的操作是原子的。若 sem value > 0,则该信 号量值减去1 并立即返回。若sem value = 0,则阻塞直到 sem value > 0,此时 立即减去 1,然后返回
b)方式二:int sem_trywait(sem_t* sem);
i)sem_trywait 函数是非阻塞的函数,它会尝试获取获取 sem value 值,如果 sem value = 0,不是阻塞住,而是直接返回一个错误 EAGAIN
(4)int sem_post(sem_t* sem);
a)把指定的信号量 sem 的值加 1,唤醒正在等待该信号量的任意线程
(5)获取信号量的值
a)int sem_getvalue(sem_t* sem, int* sval);
b)获取信号量 sem 的当前值,把该值保存在 sval,若有 1 个或者多个线程正在调用 sem_wait 阻塞在该信号量上,该函数返回阻塞在该信号量上进程或线程个数
(6)清理信号量
a)int sem_destroy(sem_t* sem);
b)成功则返回 0,失败返回 -1
2.互斥量和信号量的区别
(1)互斥量只能用于线程间的同步
(2)信号量即可以用于线程间的同步,也可以用于进程间的同步
(3)signal一个互斥量的时候,如果没有线程在wait,那么就白signal了;post一个信号量的时候,即使没有线程在wait,那该信号量的值也会加1
3.发送数据用的send函数请看第25课的第6点
4.服务器端发送数据思路
(1)将要发送的数据放到一个发消息队列里
(2)专门弄一个线程来处理发数据业务
(3)采用第29课所说的第二个方案:开始不把socket写事件通知加入到epoll,当需要写数据的时候,直接调用write/send发送数据。如果返回了EAGIN(发送缓冲区满了,需要等待可写事件才能继续往缓冲区里写数据),此时,再把写事件通知加入到epoll。此时,就变成了在epoll驱动下写数据,全部数据发送完毕后,再把写事件通知从epoll中删除
5.相关代码,以简单处理用户的注册业务为例
bool SLogic::_HandleRegister(lpngx_connection_t pCon, lpmsg_header_t pMsgHead, char* msgBody, unsigned short len)
{
if(msgBody == NULL)
{
return false;
}
if(sizeof(STRUCT_REGISTER) != len)
{
return false;
}
Lock lock(&pCon->logicProcMutex);
CCRC32* crc32 = CCRC32::GetInstance();
int sendLen = len;
char* sendBuf = static_cast<char*>(CMemory::GetInstance()->AllocMemory(m_LenMsgHeader + m_LenPkgHeader + sendLen,false)); //消息头+包头+包体
//填充消息头
memcpy(sendBuf,pMsgHead,m_LenMsgHeader);
//填充包头
lppkg_header_t pPkg = reinterpret_cast<lppkg_header_t>(sendBuf + m_LenMsgHeader);
pPkg->pkgLen = htons(m_LenPkgHeader + sendLen);
pPkg->msgCode = htons(_CMD_REGISTER);
pPkg->crc32 = htonl(crc32->Get_CRC((unsigned char*)msgBody,(unsigned int)len));
memcpy(sendBuf + m_LenMsgHeader,pPkg,m_LenPkgHeader);
//填充包体
//memcpy(sendBuf + m_LenMsgHeader + m_LenPkgHeader,msgBody,len);
strcpy(sendBuf + m_LenMsgHeader + m_LenPkgHeader,"Ok! Server has received your register request!"); //测试
sendMsg(sendBuf);
return true;
}
void CSocket::sendMsg(char *buf)
{
Lock lock(&m_sendMessageQueueMutex);
m_MsgSendQueue.push_back(buf);
++m_iSendMsgQueueCount;
if (sem_post(&m_semEventSendQueue) == -1)
{
ngx_log_show_on_stderr(errno, "File: %s Line: %d ==> Fail to call sem_post(&m_semEventSendQueue)", __FILE__, __LINE__);
}
}
//要发送数据的时候,先在这个线程入口函数里发送
//如果能够在这里把数据全部发送出去,那是最好的
//如果不能,则再在这个入口函数里往epoll中增加写事件
//当可写事件触发的时候,则会调用ngx_write_request_handler函数,这部分的数据发送是epoll驱动的
//发出去的是 包头+包体 ,不发 消息头,消息头用来检测数据包是否过期
void *CSocket::ServerSendQueueThread(void *threadData)
{
ThreadItem *pThreadItem = static_cast<ThreadItem *>(threadData);
CSocket *pSocket = pThreadItem->pThis;
lpmsg_header_t msgHeader = NULL;
lppkg_header_t pkgHeader = NULL;
std::list<char *>::iterator pos1, pos2, posend;
CMemory *memory = CMemory::GetInstance();
int err = -1;
char *toSend = NULL;
lpngx_connection_t pConn = NULL;
int n = -1; //sendproc函数返回值
while (g_stopEvent == false)
{
if (sem_wait(&pSocket->m_semEventSendQueue) == -1)
{
if (errno != EINTR)
{
ngx_log_show_on_stderr(errno, "File: %s Line: %d ==> Fail to call sem_wait(&pSocket->m_semEventSendQueue)", __FILE__, __LINE__);
}
}
if (g_stopEvent == true)
{
break;
}
if (pSocket->m_iSendMsgQueueCount > 0)
{
err = pthread_mutex_lock(&pSocket->m_sendMessageQueueMutex);
if (err != 0)
{
ngx_log_show_on_stderr(errno, "File: %s Line: %d ==> Fail to call pthread_mutex_lock(&pSocket->m_sendMessageQueueMutex)", __FILE__, __LINE__);
}
pos1 = pSocket->m_MsgSendQueue.begin();
posend = pSocket->m_MsgSendQueue.end();
while (pos1 != posend)
{
toSend = *pos1;
msgHeader = reinterpret_cast<lpmsg_header_t>(toSend); //消息头
pkgHeader = reinterpret_cast<lppkg_header_t>(toSend + pSocket->m_LenMsgHeader); //包头
pConn = msgHeader->pConn;
//数据包过期
if (pConn->iCurrsequence != msgHeader->iCurrsequence)
{
pos2 = pos1;
pos1++;
pSocket->m_MsgSendQueue.erase(pos2);
--pSocket->m_iSendMsgQueueCount;
memory->FreeMemory(toSend);
continue;
}
//要靠epoll驱动
if (pConn->iThrowsendCount > 0)
{
pos1++;
continue;
}
//走到这里,表示可以发送消息了
pConn->psendMemPointer = toSend;
pConn->psendbuf = (char *)pkgHeader;
pConn->isendlen = static_cast<unsigned int>(ntohs(pkgHeader->pkgLen));
ngx_log_show_on_stderr(0, "即将发送数据");
n = pSocket->sendproc(pConn, pConn->psendbuf, pConn->isendlen);
if (n > 0)
{
//已全部发出去
if (n == pConn->isendlen)
{
memory->FreeMemory(pConn->psendMemPointer);
pConn->psendMemPointer = NULL;
pConn->iThrowsendCount = 0;
ngx_log_show_on_stderr(0, "数据一次性发送完毕");
}
else
{
pConn->psendbuf += n;
pConn->isendlen -= n;
++pConn->iThrowsendCount;
//加入到epoll中,靠它驱动来发送
if (pSocket->ngx_epoll_oper_event(pConn->fd, EPOLL_CTL_MOD, EPOLLOUT, 0, pConn) == -1)
{
ngx_log_show_on_stderr(0, "File: %s Line: %d ==> Fail to call pSocket->ngx_epoll_oper_event(pConn->fd,EPOLL_CTL_MOD,EPOLLOUT,0,pConn)", __FILE__, __LINE__);
}
ngx_log_show_on_stderr(0, "数据没发送完毕,发送缓冲区满,整个要发送%d,实际发送了%d", pConn->isendlen, n);
}
}
else if (n == 0)
{
memory->FreeMemory(pConn->psendMemPointer);
pConn->psendMemPointer = NULL;
pConn->iThrowsendCount = 0;
ngx_log_show_on_stderr(0, "数据没发出去,连接断开了");
}
else if (n == -1)
{
++pConn->iThrowsendCount;
//加入到epoll中,靠它驱动来发送
if (pSocket->ngx_epoll_oper_event(pConn->fd, EPOLL_CTL_MOD, EPOLLOUT, 0, pConn) == -1)
{
ngx_log_show_on_stderr(0, "File: %s Line: %d ==> Fail to call pSocket->ngx_epoll_oper_event(pConn->fd,EPOLL_CTL_MOD,EPOLLOUT,0,pConn)", __FILE__, __LINE__);
}
ngx_log_show_on_stderr(0, "第一次尝试发送时缓冲区就满了,接下来靠epoll驱动");
}
else
{
memory->FreeMemory(pConn->psendMemPointer);
pConn->psendMemPointer = NULL;
pConn->iThrowsendCount = 0;
ngx_log_show_on_stderr(0, "发生了未知错误");
}
pos2 = pos1;
pos1++;
pSocket->m_MsgSendQueue.erase(pos2);
--pSocket->m_iSendMsgQueueCount;
}//end while (pos1 != posend)
err = pthread_mutex_unlock(&pSocket->m_sendMessageQueueMutex);
if (err != 0)
{
ngx_log_show_on_stderr(errno, "File: %s Line: %d ==> Fail to call pthread_mutex_unlock(&pSocket->m_sendMessageQueueMutex)", __FILE__, __LINE__);
}
}//end if (pSocket->m_iSendMsgQueueCount > 0)
}
}
ssize_t CSocket::sendproc(lpngx_connection_t c, char *buff, ssize_t size)
{
ssize_t ret = send(c->fd, c->psendbuf, c->isendlen, 0);
while (true)
{
if (ret > 0)
{
return ret;
}
//返回0一般表示连接断开
if (ret == 0)
{
return 0;
}
//缓冲区满了
if (errno == EAGAIN)
{
return -1;
}
//被信号中断,进入下一次循环
if (errno == EINTR)
{
ngx_log_show_on_stderr(errno, "File: %s Line: %d ==> errno == EINTR", __FILE__, __LINE__);
continue;
}
else
{
//其他错误统一返回-2
return -2;
}
}
}
void CSocket::ngx_write_request_handler(lpngx_connection_t pConn)
{
ssize_t n = sendproc(pConn, pConn->psendbuf, pConn->isendlen);
if ((n > 0) && (n != pConn->isendlen))
{
pConn->psendbuf += n;
pConn->isendlen -= n;
return;
}
else if (n == -1)
{
//不太可能
return;
}
//发送完毕,从epoll中删除
if ((n > 0) && (n == pConn->isendlen))
{
if (ngx_epoll_oper_event(pConn->fd, EPOLL_CTL_MOD, EPOLLOUT, 1, pConn) == -1)
{
ngx_log_show_on_stderr(0, "File: %s Line: %d ==> Fail to call ngx_epoll_oper_event(pConn->fd, EPOLL_CTL_MOD, EPOLLOUT, 1, pConn)", __FILE__, __LINE__);
}
ngx_log_show_on_stderr(0, "在epoll的驱动下,事件发送完毕");
}
//post一下,看有没有数据可以发送
if(sem_post(&m_semEventSendQueue) == -1)
{
ngx_log_show_on_stderr(0, "File: %s Line: %d ==> Fail to call sem_post(&m_semEventSendQueue)", __FILE__, __LINE__);
}
CMemory::GetInstance()->FreeMemory(pConn->pnewMemPointer);
pConn->pnewMemPointer = NULL;
--pConn->iThrowsendCount;
}
过往总结,心跳包代码实战
1.心跳包的必要性
(1)拔掉网线导致连接断开,这个行为epoll的EPOLLIN是感知不到的
2.心跳包的实现
(1)增加配置项
(2)在Socket类中增加一个std::multimap容器,键值对为
(3)增加一个处理心跳包的线程,每隔一定的时间就检测一次容器里的连接有没有按时给服务器发心跳包
3.相关代码
void CSocket::AddToTimerQueue(lpngx_connection_t pConn)
{
time_t trigTime = time(NULL);
trigTime += m_kickTimeValue;
CMemory *memory = CMemory::GetInstance();
Lock lock(&m_kickTimeMutex);
lpmsg_header_t msgHeader = reinterpret_cast<lpmsg_header_t>(memory->AllocMemory(m_LenMsgHeader, false));
msgHeader->pConn = pConn;
msgHeader->iCurrsequence = pConn->iCurrsequence;
m_kickTimeMultiMap.insert(std::make_pair(trigTime, msgHeader));
++m_kickTimeMultiMapSize;
m_firstKickTime = GetEarliestTime();
}
time_t CSocket::GetEarliestTime()
{
if (!m_kickTimeMultiMap.empty())
{
return m_kickTimeMultiMap.begin()->first;
}
else
{
return -1;
}
}
//调用者负责互斥
lpmsg_header_t CSocket::RemoveFirstTimer()
{
std::multimap<time_t, lpmsg_header_t>::iterator it;
lpmsg_header_t ret = NULL;
if (m_kickTimeMultiMapSize <= 0)
{
return NULL;
}
it = m_kickTimeMultiMap.begin();
ret = it->second;
m_kickTimeMultiMap.erase(it);
--m_kickTimeMultiMapSize;
return ret;
}
//调用者负责互斥
lpmsg_header_t CSocket::GetOverTimeTimer(time_t cur_time)
{
lpmsg_header_t ret = NULL;
CMemory *memory = CMemory::GetInstance();
if ((m_kickTimeMultiMapSize == 0) || (m_kickTimeMultiMap.empty()))
{
return NULL;
}
time_t earliestTime = GetEarliestTime();
if (earliestTime != -1)
{
//超时了
if (earliestTime <= cur_time)
{
ret = RemoveFirstTimer();
//改为下一个时间点,把该结点重新加进去,因为要继续判断
time_t newTime = cur_time + m_kickTimeValue;
lpmsg_header_t msgHeader = reinterpret_cast<lpmsg_header_t>(memory->AllocMemory(m_LenMsgHeader, false));
msgHeader->pConn = ret->pConn;
msgHeader->iCurrsequence = ret->iCurrsequence;
m_kickTimeMultiMap.insert(std::make_pair(newTime, msgHeader));
++m_kickTimeMultiMapSize;
if (m_kickTimeMultiMapSize > 0)
{
m_firstKickTime = GetEarliestTime();
}
}
}
return ret;
}
//把指定用户tcp连接从timer表中删除
void CSocket::DeleteFromTimerQueue(lpngx_connection_t pConn)
{
std::multimap<time_t, lpmsg_header_t>::iterator pos, posend;
CMemory *memory = CMemory::GetInstance();
Lock lock(&m_kickTimeMutex);
if (!m_kickTimeMultiMap.empty())
{
lblDEL:
pos = m_kickTimeMultiMap.begin();
posend = m_kickTimeMultiMap.end();
for(;pos != posend;++pos)
{
if (pos->second->pConn == pConn)
{
m_kickTimeMultiMap.erase(pos);
memory->FreeMemory(pos->second);
--m_kickTimeMultiMapSize;
goto lblDEL;
}
}
}
if (m_kickTimeMultiMapSize > 0)
{
m_firstKickTime = GetEarliestTime();
}
}
void CSocket::clearAllFromTimerQueue()
{
std::multimap<time_t, lpmsg_header_t>::iterator pos, posend;
CMemory *memory = CMemory::GetInstance();
pos = m_kickTimeMultiMap.begin();
posend = m_kickTimeMultiMap.end();
for(;pos != posend;pos++)
{
memory->FreeMemory(pos->second);
}
m_kickTimeMultiMap.clear();
m_kickTimeMultiMapSize = 0;
}
//处理心跳包的线程入口函数
void* CSocket::ServerTimerQueueMonitorThread(void *threadData)
{
ThreadItem* threadItem = reinterpret_cast<ThreadItem*>(threadData);
CSocket* pSocket = threadItem->pThis;
int err = -1;
while(g_stopEvent == false)
{
if(pSocket->m_kickTimeMultiMapSize > 0)
{
time_t trigTime = pSocket->m_firstKickTime;
time_t curTime = time(NULL);
//到时间检测心跳包了
if(trigTime <= curTime)
{
std::list<lpmsg_header_t> outTimeList;
lpmsg_header_t result;
err = pthread_mutex_lock(&pSocket->m_kickTimeMutex);
if(err != 0)
{
ngx_log_write_into_file(NGX_LOG_ERR, 0, "File: %s Line: %d ==> Fail to call pthread_mutex_lock(pSocket->m_kickTimeMutex)", __FILE__, __LINE__);
}
//取到所有超时结点
do
{
result = pSocket->GetOverTimeTimer(curTime);
if(result != NULL)
{
outTimeList.push_back(result);
}
} while (result != NULL);
err = pthread_mutex_unlock(&pSocket->m_kickTimeMutex);
if(err != 0)
{
ngx_log_write_into_file(NGX_LOG_ERR, 0, "File: %s Line: %d ==> Fail to call pthread_mutex_unlock(pSocket->m_kickTimeMutex)", __FILE__, __LINE__);
}
lpmsg_header_t tmp = NULL;
while(!outTimeList.empty())
{
tmp = outTimeList.front();
outTimeList.pop_front();
pSocket->procPingTimeOutChecking(tmp,curTime); //真正处理心跳包的函数
}
}//end if(trigTime <= curTime)
}//end if(pSocket->m_kickTimeMultiMapSize > 0)
usleep(500 * 1000); //休息500毫秒
}//end while(g_stopEvent == false)
}
void SLogic::procPingTimeOutChecking(lpmsg_header_t tmpmsg, time_t cur_time)
{
CMemory *memory = CMemory::GetInstance();
//连接没有断开
if (tmpmsg->iCurrsequence == tmpmsg->pConn->iCurrsequence)
{
if (m_ifTimeOutKick == 1)
{
ngx_log_show_on_stderr(0, "连入时长已到服务器规定最大时长,现在将连接断开");
activeCloseConnection(tmpmsg->pConn); //主动关闭连接
}
else if ((cur_time - tmpmsg->pConn->lastPingTime) > (m_kickTimeValue * 2))
{
ngx_log_show_on_stderr(0, "该连接没有按时发心跳包,要将连接断开");
activeCloseConnection(tmpmsg->pConn); //主动关闭连接
}
}
memory->FreeMemory(tmpmsg);
}
控制连入数,黑客攻击防范及畸形包应对
1.控制连入数
(1)Socket类中增加一个m_curUserCount成员,用来记录当前在线用户数量
(2)在处理新用户连入的函数中进行判断
if(m_curUserCount >= m_worker_connections)
{
close(s);
return;
}
2.flood攻击防范
(1)假设我们认为一个合理的客户端一秒钟发送数据包给服务器不超过10个。如果客户端不停的给服务器发数据包,1秒钟超过了10个数据包 ,那我服务器就认为这个玩家有恶意攻击服务器的倾向,那么服务器就果断把该连接断开
(2)实现思路:
a)在接收客户端发给服务器的数据包的时候,每当收完包头或者收完包体,我进行一 次flood攻击检测
b)检测原理:往连接结构体中增加一个成员变量,记录上一次检测的时间,当前在检 测的时候,如果当前时间跟上次检测的时间小于配置文件的允许的最小时间间隔,而且 次数超过一定数量,那么就认定该客户端有flood攻击嫌疑,直接断开与该客户端的连 接
(3)相关配置
(4)相关代码
3.畸形数据包
(1)在收数据的时候,比如要收一个字符数组,那么该数组内应该有一个字符串结束标志’\0’。如果有恶意客户端没有加上该标志,那么服务器在读该字符数组的时候,就会发生内存访问越界的错误。
(2)防范
a)对于要接收字符数组的时候,服务器收到后在读取之前,强制性在数组的最后一个 元素的位置加上字符串结束标志,再进行内容识别
4.超时直接提出服务器的需求
(1)比如一个服务器是用来做账户相关的验证的,那么客户端在验证完毕后就没有必要一直连着服务器不断开,这样会消耗服务器资源。当客户端连入一定的时间长度后,服务器端应该主动与该客户端连接断开
(2)实现思路
a)只需要在处理心跳包的代码基础上进行修改,如下
超负荷安全处理,综合压力测试
1.测试程序
(1)自己用Qt写了一个多线程模拟大量用户不定时连入、收发数据、断开的测试程序
2.epoll限速思路
(1)当收到大量数据包时,就把epoll中的EPOLLIN通知先临时删除掉。当处理完当前的数据包时,再加回去
3.积压太多数据包发送不出去时,为了服务器稳定,规定一个数值,当积压的数据包大于该数值时,就把后续发送过来的数据包先丢弃
4.连入安全的进一步完善
(1)有新用户连进来的时候,从连接池中拿出来一个连接结点与该用户绑定。当连接池中的可用连接用完的时候,本项目的策略是,新new一个新的连接池结点出来,避免用户连入失败,这里就有服务器内存耗尽的隐患。
(2)为了服务器更稳定,在用户连入的时候加入下面的判断
if(totalConnCount > (m_worker_connections * 3))
{
if(freeConnCount < m_worker_connections)
{
close(s);
return;
}
}
惊群,性能优化大局观
1.动态查看CPU占比
(1)top -p pid
2.惊群
(1)一个socket连入,惊动N个worker进程,但是只有一个worker进程成功accept
(2)惊群是操作系统的缺陷
(3)解决办法:
a)给进程加锁,获得锁的进程才往epoll中增加EPOLLIN
b)内核版本在3.9以上时,在操作系统层面已解决惊群问题。其中的原理为端口复用, 是一种套接字的复用机制,允许将多个套接字bind到同一个ip地址/端口上
setsockopt(isock, SOL_SOCKET, SO_REUSEPORT,(const void *) &reuseport, sizeof(int));
3.性能优化大局观
(1)软件层面
a)充分利用CPU,一个进程只绑定在一个CPU上。CPU是有缓存的,增加CPU缓存命 中率可提高性能
b)充分了解TCP/IP协议,然后进行相关参数配置
c)优化业务逻辑算法
d)提升进程优先级。查看进程上下文切换次数:pidstat - w - p pid 1
(2)硬件层面
a)使用高速网卡,增加网络带宽
b)使用专业服务器,加大内存
4.配置最大允许打开的文件句柄数
(1)查看操作系统可以使用的最大句柄数:cat /proc/sys/fs/file-max
(2)查看当前已经分配的,分配了没使用的,文件句柄最大数目:cat /proc/sys/fs/file-nr
(3)限制用户使用的最大句柄数
a)查看系统允许的当前用户进程打开的文件数限制
ulimit -n
b)临时设置,只对当前session有效
ulimit -HSn 数量
5.ulimit详解
(1)语法格式:ulimit [-acdfHlmnpsStvw] [size]
(2)参数介绍
-H 设置硬件资源限制.
-S 设置软件资源限制.
-a 显示当前所有的资源限制.
-c size:设置core文件的最大值.单位:blocks
-d size:设置数据段的最大值.单位:kbytes
-f size:设置创建文件的最大值.单位:blocks
-l size:设置在内存中锁定进程的最大值.单位:kbytes
-m size:设置可以使用的常驻内存的最大值.单位:kbytes
-n size:设置内核可以同时打开的文件描述符的最大值.单位:n
-p size:设置管道缓冲区的最大值.单位:kbytes
-s size:设置堆栈的最大值.单位:kbytes
-t size:设置CPU使用时间的最大上限.单位:seconds
-v size:设置虚拟内存的最大值.单位:kbytes
-u number:设置用户最大进程数 (max user processes)
(3)例子
a)Linux对于每个用户,系统限制其最大进程数。为提高性能,可以根据设备资源情况, 设置各linux 用户的最大进程数,下面我把某linux用户的最大进程数设为10000个:
ulimit -u 10000
b)对于需要做许多 socket 连接并使它们处于打开状态的 Java 应用程序而言,最好通 过使用 ulimit -n xx 修改每个进程可打开的文件数,缺省值是 1024。将每个进程可以打 开的文件数目加大到4096,缺省为1024
ulimit -n 4096
c)其他建议设置成无限制(unlimited)的一些重要设置是:
数据段长度:ulimit -d unlimited
最大内存大小:ulimit -m unlimited
堆栈大小:ulimit -s unlimited
CPU 时间:ulimit -t unlimited
虚拟内存:ulimit -v unlimited
(4)解除 Linux 系统的最大进程数和最大文件打开数限制
方法一:
a)vi /etc/security/limits.conf
b)添加
方法二:
a)vi /etc/profile
b)添加
ulimit -u 65535
ulimit -n 65535
ulimit -d unlimited
ulimit -m unlimited
ulimit -s unlimited
ulimit -t unlimited
ulimit -v unlimited
c)source /etc/profile
6.可根据实际需要,使用内存池进行内存方面的维护。但服务器处理业务时要经常new和delete,内存池起到的作用不是很大,可看情况决定