GitHub上的项目:qinguoyi/TinyWebServer: Linux下C++轻量级Web服务器学习 (github.com),这篇博客记录一下follow的日程和更详细的注解和逻辑思考。整个工程作者没有透露完成顺序,我就根据自己的理解从一个部分开始逐步往下。
第一站
lock
服务器需要一些互斥操作,因为一些共享资源(如数据库连接池、线程池)被同时访问时会出现错误,需要互斥访问。因此互斥作为一个小的辅助功能,在前面这里先进行分析。
locker.h代码如下:
1 |
|
互斥锁mutex
互斥锁主要是让一个资源锁起来,同一时间只能有一个活动在使用这个资源,其他的请求全部被卡住。项目中具体的实现不是用零散的mutex类的函数操作,而是用一个locker类封装好,构造和析构函数分别执行初始化和注销,这使得用户不需要手动去做(RAII思想,同时可以简化API较长的函数名,其他两个类也是这样的思想)。
- int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
- pthread_mutex_init() 函数是以动态方式创建互斥锁的,参数attr指定了新建互斥锁的属性。如果参数attr为空(NULL),则使用默认的互斥锁属性,默认属性为快速互斥锁 。互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。
- pthread_mutexattr_init() 函数成功完成之后会返回零,其他任何返回值都表示出现了错误。函数成功执行后,互斥锁被初始化为未锁住态。
- *pthread_mutex_destroy()*用于注销一个互斥锁,API定义如下:int pthread_mutex_destroy(pthread_mutex_t *mutex*)*
- 销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。由于在Linux中,互斥锁并不占用任何资源,因此LinuxThreads中的 pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。
- int pthread_mutex_lock(pthread_mutex_t *mutex):锁住,返回值为0成功;
- int pthread_mutex_unlock(pthread_mutex_t *mutex):解锁,返回值为0成功;
简单来说:
- pthread_mutex_init函数用于初始化互斥锁
- pthread_mutex_destory函数用于销毁互斥锁
- pthread_mutex_lock函数以原子操作方式给互斥锁加锁
- pthread_mutex_unlock函数以原子操作方式给互斥锁解锁
信号量sem
信号量有数值大小,主要用来管理一个buffer,写入和读出都要满足buffer的边界,同样的取用资源也要在资源池满足的情况下进行。
- int sem_init(sem_t *sem, int pshared, unsigned int value);
- 该函数初始化由 sem 指向的信号对象,并给它一个初始的整数值 value。pshared 控制信号量的类型,值为 0 代表该信号量用于多线程间的同步,值如果大于 0 表示可以共享,用于多个相关进程间的同步:参数 pshared > 0 时指定了 sem 处于共享内存区域,所以可以在进程间共享该变量
- int sem_destroy(sem_t *sem);
- 该函数用于对用完的信号量的清理。
- int sem_wait(sem_t *sem);
- sem_wait 是一个阻塞的函数,测试所指定信号量的值,它的操作是原子的。若 sem value > 0,则该信号量值减去 1 并立即返回。若sem value = 0,则阻塞直到 sem value > 0,此时立即减去 1,然后返回。函数成功返回0,错误的话信号量的值不改动,返回-1。
- 还有另一个函数:sem_trywait 函数是非阻塞的函数,它会尝试获取获取 sem value 值,如果 sem value = 0,不是阻塞住,而是直接返回一个错误 EAGAIN。
- int sem_post(sem_t *sem);
- 把指定的信号量 sem 的值加 1,唤醒正在等待该信号量的任意线程。成功时返回 0;错误时,信号量的值没有更改,-1 被返回。
简单来说:
- sem_init函数用于初始化一个未命名的信号量
- sem_destory函数用于销毁信号量
- sem_wait函数将以原子操作方式将信号量减一,信号量为0时,sem_wait阻塞
- sem_post函数以原子操作方式将信号量加一,信号量大于0时,唤醒调用sem_post的线程
条件变量cond
int pthread_cond_init(pthread_cond_t *cv, const pthread_condattr_t *cattr);
- 返回值:函数成功返回0;任何其他返回值都表示错误。初始化一个条件变量。当参数cattr为空指针时,函数创建的是一个缺省的条件变量。否则条件变量的属性将由cattr中的属性值来决定。调用 pthread_cond_init函数时,参数cattr为空指针等价于cattr中的属性为缺省属性,只是前者不需要cattr所占用的内存开销。这个函数返回时,条件变量被存放在参数cv指向的内存中。
int pthread_cond_destroy(pthread_cond_t *cv);
- 返回值:函数成功返回0;任何其他返回值都表示错误。释放条件变量。需要注意的是只有在没有线程在该条件变量上等待时,才可以注销条件变量,否则会返回EBUSY。同时Linux在实现条件变量时,并没有为条件变量分配资源,所以在销毁一个条件变量时,只要注意该变量是否仍有等待线程即可。
int pthread_cond_wait(pthread_cond_t *cv, pthread_mutex_t *mutex);
返回值:函数成功返回0;任何其他返回值都表示错误。
为什么要关联一个mutex呢?无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或pthread_cond_timedwait(),下同)的竞争条件(Race Condition)。
- pthread_cond_wait(cond, mutex)的功能有3个:
- 调用者线程首先释放mutex
- 然后阻塞,等待被别的线程唤醒
- 当调用者线程被唤醒后,调用者线程会再次获取mutex
- pthread_cond_wait(cond)的功能只有1个:
- 调用者线程阻塞,等待被别的线程唤醒。
这里首先给一个简洁的回答:
- 通常的应用场景下,当前线程执行pthread_cond_wait时,处于临界区访问共享资源,存在一个mutex与该临界区相关联,这是理解pthread_cond_wait带有mutex参数的关键
- 当前线程执行pthread_cond_wait前,已经获得了和临界区相关联的mutex;因为缺少其他条件,执行pthread_cond_wait会阻塞,但是在进入阻塞状态前,必须释放已经获得的mutex,让其它线程能够进入临界区
- 当前线程执行pthread_cond_wait后,阻塞等待的条件满足,条件满足时会被唤醒;被唤醒后,仍然处于临界区,因此被唤醒后必须再次获得和临界区相关联的mutex
综上,调用pthread_cond_wait时,线程总是位于某个临界区,该临界区与mutex相关,pthread_cond_wait需要带有一个参数mutex,用于释放和再次获取mutex。
- pthread_cond_wait(cond, mutex)的功能有3个:
int pthread_cond_timedwait(pthread_cond_t *cv,pthread_mutex_t *mp, const struct timespec * abstime);
- 返回值:函数成功返回0;任何其他返回值都表示错误
- pthread_cond_timedwait()用于等待一个条件变量,等待条件变量的同时可以设置等待超时。这是一个非常有用的功能,如果不想一直等待某一条件变量,就可以使用这个函数。函数到了一定的时间,即使条件未发生也会解除阻塞。
- 条件变量默认使用的时间是CLOCK_REALTIME。通过clock_gettime()接口获取时间。
int pthread_cond_signal(pthread_cond_t *cv);
- 返回值:函数成功返回0;任何其他返回值都表示错误
- 函数发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行。如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。
int pthread_cond_broadcast(pthread_cond_t *cv);
- 返回值:函数成功返回0;任何其他返回值都表示错误
- 函数唤醒所有被pthread_cond_wait函数阻塞在某个条件变量上的线程,参数cv被用来指定这个条件变量。当没有线程阻塞在这个条件变量上时,pthread_cond_broadcast函数无效。
线程池threadpool
使用线程有动态方法和静态方法,动态方法是当一个工作需要完成时创建一个线程,当工作做完后释放线程。这种方式对资源的利用率高一些,但是耗费时间,因为要新创建、销毁线程。静态方法是使用线程池先创建好一系列等待请求的线程,当一个工作到来时直接分配空闲线程,工作完成后放回线程池。
线程池的代码放在threadpool.h中,代码如下:
1 |
|
- 线程池类使用模板,目前还没看出作用,猜测是后面会用于多种不同资源的分配使用,如处理http连接、处理数据库请求等等。
- int
pthread_create(pthread_t *tidp,constpthread_attr_t *attr,(void*)(*start_rtn)(void*),void*arg);- 第一个参数为指向线程 标识符的指针。
- 第二个参数用来设置线程属性。
- 第三个参数是线程运行函数的起始地址(函数指针)。
- 最后一个参数是运行函数的参数。
- 若线程创建成功,则返回0。若线程创建失败,则返回出错编号,并且*thread中的内容是未定义的。
- int pthread_detach(pthread_t thread); 成功:0;失败:错误号
- 使用pthread_create创建的线程有两种状态:joinable和unjoinable。默认是joinable 状态。
- 线程创建后在线程中调用 pthread_detach, 如:pthread_detach(pthread_self()),将状态改为unjoinable状态,确保资源的释放。
- pthread_detach()和pthread_join()就是控制子线程回收资源的两种不同的方式。同一进程间的线程具有共享和独立的资源,其中共享的资源有堆、全局变量、静态变量、文件等公用资源。而独享的资源有栈和寄存器,这两种方式就是决定子线程结束时如何回收独享的资源。
- 如果是joinable状态,则该线程结束后(通过pthread_exit结束或者线程执行体任务执行完毕)不会释放线程所占用堆栈和线程描述符(总计8K多)等资源,除非在主线程调用了pthread_join函数之后才会释放。pthread_join函数一般应用在主线程需要等待子线程结束后才继续执行的场景。(pthread_join是一个阻塞函数,调用方会阻塞到pthread_join所指定的tid的线程结束后才被回收,但是在此之前,调用方是霸占系统资源的。 )
- 如果是unjoinable状态,则该线程结束后会自动释放占用资源。实现方式是在创建时指定属性,或者在线程执行体的最开始处添加一行:pthread_detach(pthread_self());不会阻塞,调用它后,线程运行结束后会自动释放资源,后者非常方便。
- 总结
- pthread_detach()即主线程与子线程分离,两者相互不干涉,子线程结束同时子线程的资源自动回收。
- pthread_join()即是子线程合入主线程,主线程会一直阻塞,直到子线程执行结束,然后回收子线程资源,并继续执行。
- 工作流程:
- 构造函数初始化线程池:创建线程和分离线程状态
- 析构函数销毁线程池
- append函数互斥地向list添加请求,并post信号量
- 一个线程对应一个worker,worker函数调用run。
- run函数从list互斥地获得请求并工作,不断循环
- worker函数是一个成员函数,那么必须是一个静态的。它是一个回调函数,回调函数是通过指针调用的函数,最常使用的回调函数就是在创建线程时(pthread_create),以一个函数指针以及传递给这个函数多个参数来调用线程函数来创建线程。那么一般的类成员函数是不能用作回调函数的,因为在使用回调函数时,会传递指定的符合回调函数声明的的参数给回调函数,而类成员函数隐式包含一个this指针参数,所以把类成员函数当作回调函数编译时会因为参数不匹配会出错(回调后多了个this,与声明不一致)。
- 静态成员函数就没有这个问题,里面没有this指针。
- 那么为什么要用worker间接调用run函数呢?run设计成静态的直接调用不行吗?
- 答案是不太方便,因为静态成员函数只能访问静态成员数据、其他静态成员和类外部的函数,因为没有this指针。不过我们这里手动传入了this指针使得它可以调用run成员函数。
- this指针只能在类内部使用而不能在外部使用。可以访问类中所有public、private、protect的成员函数和变量。this指针是指向对象的实例,所以只有当对象被创建时this指针才有效。
- 同一个模板类的不同实例共享静态成员函数,不同实例有不同的资源,这导致静态成员函数不能访问那些实例各有的资源,因为不知道要访问哪个。而run要操作不同实例的list等等资源,通过共享的worker使用从线程传入的this指针操作各个实例的run函数,run就能操作自己这个实例的资源了。但如果run是静态的,即使通过手动传入this参数,run里面所有的资源都要this->一下,太不方便了。
- 因此,最好的方式就是静态成员函数通过this指针调用成员函数,这个成员函数就可以很方便地访问类实例的资源了(说白了就是使用资源不用this->了)。
第二站
单例模式
后面会用到单例模式,这里先详解一下,参考了许多文章。
什么是单例模式?
保证整个系统中一个类只有一个对象的实例,实现这种功能的方式就叫单例模式。
为什么要用单例模式?
1、单例模式节省公共资源
比如:大家都要喝水,但是没必要每人家里都打一口井是吧,通常的做法是整个村里打一个井就够了,大家都从这个井里面打水喝。
对应到我们计算机里面,像日志管理、打印机、数据库连接池、应用配置。
2、单例模式方便控制
就像日志管理,如果多个人同时来写日志,你一笔我一笔那整个日志文件都乱七八糟,如果想要控制日志的正确性,那么必须要对关键的代码进行上锁,只能一个一个按照顺序来写,而单例模式只有一个人来向日志里写入信息方便控制,避免了这种多人干扰的问题出现。
实现单例模式的思路
1. 构造私有:
如果要保证一个类不能多次被实例化,那么我肯定要阻止对象被new 出来,所以需要把类的所有构造方法私有化。
2.以静态方法返回实例。
因为外界就不能通过new来获得对象,所以我们要通过提供类的方法来让外界获取对象实例。
3.确保对象实例只有一个。
只对类进行一次实例化,以后都直接获取第一次实例化的对象。
1 | /** |
这里类的实例在类初始化的时候已经生成,不再进行第二次实例化了,而外界只能通过SingleCase.getInstance()方法来获取SingleCase对象, 所以这样就保证整个系统只能获取一个类的对象实例。
单例模式的两种实现模式
饿汉模式:就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。
1 | 优点:简单 |
1 | class Singleton |
如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。
懒汉模式:等到用的的时候程序再创建实例对象
1 | 优点:第一次使用实例对象时,创建对象。进程启动无负载。多个单例实例启动顺序自由控制。 |
1 |
|
添加一个类的静态对象,总是让人不太满意,所以有人用如下方法来重新实现单例和解决它相应的问题,代码如下:
1 | class CSingleton |
使用局部静态变量是非常强大的方法,完全实现了单例的特性,而且代码量更少,也不用担心单例销毁的问题。
sql数据库连接
头文件
数据库连接的头文件声明了很多信息,我们先分析头文件的逻辑,再去看定义的实现
1 |
|
头文件里主要是对connection_pool这个类的功能进行了声明:
- 四种主要功能:获取数据库连接、获取空闲连接数目、释放连接、销毁所有连接。四个功能函数实际上从返回值就可以看出区别和要做什么事。
- 数据库初始化init函数,它通过地址、端口、用户名密码、使用的数据库名称来进行数据库的连接。
- 单例模式,把构造函数放private,使得只能用静态成员函数在类中创建类对象;把析构函数放private,使得无法在外部delete类对象,只能用内部的成员函数delete this,因为内部成员函数才能访问私有的析构函数。不过这里的单例模式不用new,因此也就没有对应的delete函数。
还有一个connectionRAII类,这个类对连接池对象进行RAII式的管理,前面可以看到有个释放连接的功能,我们不想手动释放,就可以在这个类的析构函数里释放,具体看实现就好了。
- 为什么con是双指针,可以参考c/c++向函数传递指针并修改其指向的问题_AlanChaw292的博客-CSDN博客_c++改变指针指向。大概的意思就是,如果是单指针传进来,编译器也会为形参做一个备份,如传入一个p,会备份一个p1(我们实际上使用的是p1,跟值传递是一个意思),p和p1的值相同,都指向对象的地址。我们当然可以使用p1来修改指向的值,但无法通过修改p1修改p(就像形参无法影响实参),也就是说传入的指针不能修改指针本身的地址(不能修改指针的指向,不是不允许,而是没意义)。这种时候,就需要用双指针,指向我们想修改的指针的地址,这样就行了,那篇博客讲的很清楚。
.cpp实现
话不多说,先上源码
1 |
|
- MYSQL *mysql_init(MYSQL *mysql)
- 分配或初始化与mysql_real_connect()相适应的MYSQL对象。如果mysql是NULL指针,该函数将分配、初始化、并返回新对象。否则,将初始化对象,并返回对象的地址。如果mysql_init()分配了新的对象,当调用mysql_close()来关闭连接时。将释放该对象。
- 返回值:初始化的MYSQL*句柄。如果无足够内存以分配新的对象,返回NULL。
- MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user, const char *passwd, const char *db, unsigned int port, const char *unix_socket, unsigned long client_flag)
- mysql_real_connect()尝试与运行在主机上的MySQL数据库引擎建立连接。在你能够执行需要有效MySQL连接句柄结构的任何其他API函数之前,mysql_real_connect()必须成功完成。
- 参数:
- 第1个参数应是已有MYSQL结构的地址。调用mysql_real_connect()之前,必须调用mysql_init()来初始化MYSQL结构。通过mysql_options()调用,可更改多种连接选项。
- “host”的值必须是主机名或IP地址。如果“host”是NULL或字符串”localhost”,连接将被视为与本地主机的连接。如果操作系统支持套接字(Unix)或命名管道(Windows),将使用它们而不是TCP/IP连接到服务器。
- “user”参数包含用户的MySQL登录ID。如果“user”是NULL或空字符串””,用户将被视为当前用户。在UNIX环境下,它是当前的登录名。在Windows ODBC下,必须明确指定当前用户名。
- “passwd”参数包含用户的密码。如果“passwd”是NULL,仅会对该用户的(拥有1个空密码字段的)用户表中的条目进行匹配检查。这样,数据库管理员就能按特定的方式设置MySQL权限系统,根据用户是否拥有指定的密码,用户将获得不同的权限。
- “db”是数据库名称。如果db为NULL,连接会将默认的数据库设为该值。
- 如果“port”不是0,其值将用作TCP/IP连接的端口号。注意,“host”参数决定了连接的类型。
- 如果unix_socket不是NULL,该字符串描述了应使用的套接字或命名管道。注意,“host”参数决定了连接的类型。
- client_flag的值通常为0,其他标志可以实现特定的功能
- 返回值:如果连接成功,返回MYSQL*连接句柄。如果连接失败,返回NULL。对于成功的连接,返回值与第1个参数的值相同。
- void mysql_close(MYSQL *mysql)
- 关闭前面打开的连接。如果句柄是由mysql_init()或mysql_connect()自动分配的,mysql_close()还将解除分配由mysql指向的连接句柄。
- string.c_str():
- const char *c_str();
- c_str()函数返回一个指向正规C字符串的指针常量, 内容与本string串相同。
- 这是为了与c语言兼容,在c语言中没有string类型,故必须通过string类对象的成员函数c_str()把string 对象转换成c中的字符串样式。
第三站
日志系统
这里可以看看作者的讲解先理解一下:
最新版Web服务器项目详解 - 09 日志系统(上) (qq.com)
最新版Web服务器项目详解 - 10 日志系统(下) (qq.com)
阻塞队列
写入日志有同步的写入和异步的写入方式,同步的方式是当产生日志时就写入,主线程工作推迟;异步的写入方式是使用一个日志线程来管理,“写入日志”这个任务就需要有地方放,因此就要用一个阻塞队列来存放任务。为什么是阻塞的呢,日志线程有多个吗?实际上日志线程只有一个,但其他线程可以有多个,这就是一个多生产者–单消费者的模型。因此常规的解法就是用互斥锁+buffer信号量的组合。但这个阻塞队列还添加了超时处理的功能,信号量就需要改成条件变量,条件变量我没怎么使用过,之后回过头再整理一下,不过具体的功能在lock那章中写了。
下面是阻塞队列的代码,在头文件block_queue.h中
1 | /************************************************************* |
在C语言中可以使用函数gettimeofday()函数来得到精确时间。它的精度可以达到微妙,是C标准库的函数。
在gettimeofday()函数中tv或者tz都可以为空。如果为空则就不返回其对应的结构体。
函数执行成功后返回0,失败后返回-1,错误代码存于errno中。
1 |
|
说明:在使用gettimeofday()函数时,第二个参数一般都为空,因为我们一般都只是为了获得当前时间,而不用获得timezone的数值。
头文件
定义了Log类,其中使用宏来为其他程序提供接口。
1 |
|
.cpp实现
主要是一些string的操作,因为写日志就是把字符写入文件嘛
1 |
|
涉及到的与时间类相关的知识:
struct tm *localtime(const time_t *timer)
timer – 这是指向表示日历时间的 time_t 值的指针。
C 库函数 struct tm *localtime(const time_t *timer) 使用 timer 的值来填充 tm 结构。timer 的值被分解为 tm 结构,并用本地时区表示。
该函数返回指向 tm 结构的指针,该结构带有被填充的时间信息。下面是 tm 结构的细节:
1
2
3
4
5
6
7
8
9
10
11struct 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 到 11 */
int tm_year; /* 自 1900 起的年数 */
int tm_wday; /* 一周中的第几天,范围从 0 到 6 */
int tm_yday; /* 一年中的第几天,范围从 0 到 365 */
int tm_isdst; /* 夏令时 */
};char *strrchr(const char *str, int c)
C 库函数 char *strrchr(const char *str, int c) 在参数 str 所指向的字符串中搜索最后一次出现字符 c(一个无符号字符)的位置。
该函数返回 str 中最后一次出现字符 c 的位置。如果未找到该值,则函数返回一个空指针。
int snprintf ( char * str, size_t size, const char * format, … );
C 库函数 int snprintf(char *str, size_t size, const char *format, …) 设将可变参数**(…)按照 format 格式化成字符串,并将字符串复制到 str 中,size** 为要写入的字符的最大数目,超过 size 会被截断。
返回值
- 1、如果格式化后的字符串长度小于 size,则会把字符串全部复制到 str 中,并给其后添加一个字符串结束符 \0;
- 2、如果格式化后的字符串长度大于等于 size,超过 size 的部分会被截断,只将其中的 (size-1) 个字符复制到 str 中,并给其后添加一个字符串结束符 \0,返回值为欲写入的字符串长度。
VA_LIST 解决变参问题的一组宏,所在头文件:
#include <stdarg.h>,用于获取不确定个数的参数,这种获取是根据参数类型对应的大小,找到对应的内存地址,然后获取参数来实现的typedef char *va_list; 获取可变参数列表的第一个参数的地址(list是类型为va_list的指针,param1是可变参数最左边的参数) #define va_start(list,param1) 获取可变参数的当前参数,返回指定类型并将指针指向下一参数(mode参数描述了当前参数的类型) #define va_arg(list,mode) 清空va_list可变参数列表 #define va_end(list)1
2
3
4
5
6
7
* ```
va_list的使用方法:
a) 首先在函数中定义一个具有va_list型的变量,这个变量是指向参数的指针。
b) 然后用va_start宏初始化变量刚定义的va_list变量,使其指向第一个可变参数的地址。
c) 然后va_arg返回可变参数,va_arg的第二个参数是你要返回的参数的类型(如果多个可变参数,依次调用va_arg获取各个参数)。
d) 最后使用va_end宏结束可变参数的获取。int vsnprintf (char * s, size_t n, const char * format, va_list arg );
将格式化的数据从变量参数列表写入大小已设置的缓冲区
参数
s
指向存储结果C-string的缓冲区的指针。 缓冲区的大小至少应为n字符。
n
缓冲区中要使用的最大字节数。 生成的字符串的长度最大为
n-1,为其他终止空字符留出空间。format
包含格式字符串的C字符串,其格式与prinf相同。
arg
一个值,该值标识用初始化的变量参数列表。
返回值
- 成功打印到sbuf中的字符的个数,不包括末尾追加的\0。如果格式化解析失败,则返回负数。
第四站
http——前置知识
这部分内容很多,耐心些,别急
作者写的三篇介绍:
最新版Web服务器项目详解 - 04 http连接处理(上) (qq.com)
最新版Web服务器项目详解 - 05 http连接处理(中) (qq.com)
最新版Web服务器项目详解 - 06 http连接处理(下) (qq.com)
epoll
epoll是linux新内核中替换select来做事件触发的机制,效率非常高,底层使用红黑树实现。这篇博客讲的非常清楚,强烈推荐:epoll使用详解(精髓)_ljx0305的博客-CSDN博客_epoll。下面简单介绍下API,头文件#include <sys/epoll.h>。
int epoll_create(int size)
- 创建一个指示epoll内核事件表的文件描述符,该描述符将用作其他epoll系统调用的第一个参数,size不起作用。(从Linux 2.6.8开始,max_size参数将被忽略,但必须大于零。)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
该函数用于操作内核事件表监控的文件描述符上的事件:注册、修改、删除
epfd:为epoll_creat的句柄
op:表示动作,用3个宏来表示:
- EPOLL_CTL_ADD (注册新的fd到epfd),相当于把fd加到epfd这棵红黑树上
- EPOLL_CTL_MOD (修改已经注册的fd的监听事件),
- EPOLL_CTL_DEL (从epfd删除一个fd);
fd:文件描述符
event:告诉内核需要监听的事件,结构如下:
1
2
3
4
5
6
7
8
9
10
11typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; /* Epoll events,是一串比特,设置类型时把类型或起来 */
epoll_data_t data; /* User data variable */
};events描述事件类型,其中epoll事件类型有以下几种
- EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
- EPOLLOUT:表示对应的文件描述符可以写
- EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
- EPOLLERR:表示对应的文件描述符发生错误
- EPOLLHUP:表示对应的文件描述符被挂断;
- EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的
- EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
该函数用于等待所监控文件描述符上有事件的产生,返回就绪的文件描述符个数
events:用来存内核得到事件的集合,
maxevents:告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,
timeout:是超时时间
- -1:阻塞
- 0:立即返回,非阻塞
- >0:指定毫秒
返回值:成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1
触发模式:
LT水平触发模式
- 当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll还会再次向应用程序通知此事件,直到该事件被处理完毕。
ET边缘触发模式
- 当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。
- 必须要一次性将数据读取完,使用非阻塞I/O,读取到出现eagain
ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,故效率要比LT模式高。LT模式是epoll的默认工作模式
EPOLLONESHOT
- 一个线程读取某个socket上的数据后开始处理数据,在处理过程中该socket上又有新数据可读,此时另一个线程被唤醒读取,此时出现两个线程处理同一个socket
- 我们期望的是一个socket连接在任一时刻都只被一个线程处理,通过epoll_ctl对该文件描述符注册epolloneshot事件,一个线程处理socket时,其他线程将无法处理,当该线程处理完后,需要通过epoll_ctl重置epolloneshot事件
项目中epoll相关代码部分包括非阻塞模式、内核事件表注册事件、删除事件、重置EPOLLONESHOT事件四种。
非阻塞模式
//对文件描述符设置非阻塞 int setnonblocking(int fd) { int old_option = fcntl(fd, F_GETFL);//返回fd的状态标志,是一串比特位 int new_option = old_option | O_NONBLOCK;//设置非阻塞的比特位,把前面获得的flag和它或起来就可以了 fcntl(fd, F_SETFL, new_option);//重新设置 return old_option; } /* 阻塞方式是文件读写操作的默认方式,但是应用程序员可通过使用O_NONBLOCK 标志来人为 的设置读写操作为非阻塞方式 .( 该标志定义在 < linux/fcntl.h > 中,在打开文件时指定 ) . 如果设置了 O_NONBLOCK 标志,read 和 write 的行为是不同的 ,如果进程没有数据就绪时调用了 read , 或者在缓冲区没有空间时调用了 write ,系统只是简单的返回 EAGAIN,而不会阻塞进程. */fcntl函数功能依据cmd的值的不同而不同。参数对应功能如下:1
2
3
4
5
6
7
8
9
fcntl系统调用可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性
```c++
#include<unistd.h>
#include<fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd ,struct flock* lock);返回值:与命令有关。如果出错,所有命令都返回-1,如果成功则返回某个其他值。下列三个命令有特定返回值:F_DUPFD,F_GETFD,F_GETFL以及F_GETOWN。第一个返回新的文件描述符,第二个返回相应标志,最后一个返回一个正的进程ID或负的进程组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(1)F_DUPFD
与dup函数功能一样,复制由fd指向的文件描述符,调用成功后返回新的文件描述符,与旧的文件描述符共同指向同一个文件。
(2)F_GETFD
读取文件描述符close-on-exec标志。
close_on_exec 是一个进程所有文件描述符(文件句柄)的位图标志,每个比特位代表一个打开的文件描述符,用于确定在调用系统调用execve()时需要关闭的文件句柄(参见include/fcntl.h)。当一个程序使用fork()函数创建了一个子进程时,通常会在该子进程中调用execve()函数加载执行另一个新程序。此时子进程将完全被新程序替换掉,并在子进程中开始执行新程序。若一个文件描述符在close_on_exec中的对应比特位被设置,那么在执行execve()时该描述符将被关闭,否则该描述符将始终处于打开状态。
试想一下这样的场景:在Webserver中,首先会使用root权限启动,以此打开root权限才能打开的端口、日志等文件。然后降权到普通用户,fork出一些worker进程,这些进程中再进行解析脚本、写日志、输出结果等进一步操作。
然而这里,就会发现隐含一个安全问题:子进程中既然继承了父进程的FD,那么子进程中运行的脚本只需要继续操作这些FD,就能够使用普通权限“越权”操作root用户才能操作的文件。
(3)F_SETFD
将文件描述符close-on-exec标志设置为第三个参数arg的最后一位
(4)F_GETFL
获取文件打开方式的标志,标志值含义与open调用一致
(5)F_SETFL
设置文件打开方式标志为arg指定方式
(6)F_SETLK
此时fcntl函数用来设置或释放锁。当short_l_type为F_RDLCK为读锁,F_WDLCK为写锁,F_UNLCK为解锁。
如果锁被其他进程占用,则返回-1;
这种情况设的锁遇到锁被其他进程占用时,会立刻停止进程。
(7)F_SETLKW
此时也是给文件上锁,不同于F_SETLK的是,该上锁是阻塞方式。当希望设置的锁因为其他锁而被阻止设置时,该命令会等待相冲突的锁被释放。
(8)F_GETLK
第3个参数lock指向一个希望设置的锁的属性结构,如果锁能被设置,该命令并不真的设置锁,而是只修改lock的l_type为F_UNLCK,然后返回该结构体。如果存在一个或多个锁与希望设置的锁相互冲突,则fcntl返回其中的一个锁的flock结构。内核事件表注册新事件,开启EPOLLONESHOT,针对客户端连接的描述符,listenfd不用开启
void addfd(int epollfd, int fd, bool one_shot) { epoll_event event; event.data.fd = fd; #ifdef ET event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;//告知要监听可读事件、文件描述符被挂断事件 #endif #ifdef LT event.events = EPOLLIN | EPOLLRDHUP;//可读、文件描述符被挂断 #endif if (one_shot) event.events |= EPOLLONESHOT; epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);//处理好描述的事件类型后,添加进内核事件表的文件描述符epfd setnonblocking(fd); }1
2
3
4
5
6
7
8
9
* 内核事件表删除事件
* ```c++
void removefd(int epollfd, int fd)
{
epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);//标志是delete,删除这个fd
close(fd);
}重置EPOLLONESHOT事件
```c++
void modfd(int epollfd, int fd, int ev)
{
epoll_event event;
event.data.fd = fd;
#ifdef ET
event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
#endif#ifdef LT
event.events = ev | EPOLLONESHOT | EPOLLRDHUP;
#endifepoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);}
1
2
3
4
5
### http细节
请求报文:get和post,报文的请求头部不一定全部都有,但可以有:GET /562f25980001b1b106000338.jpg HTTP/1.1
Host:img.mukewang.com
User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
Accept:image/webp,image/,/*;q=0.8
Referer:http://www.imooc.com/
Accept-Encoding:gzip, deflate, sdch
Accept-Language:zh-CN,zh;q=0.8
空行
请求数据为空
1 |
POST / HTTP1.1
Host:www.wrox.com
User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
Content-Type:application/x-www-form-urlencoded
Content-Length:40
Connection: Keep-Alive
空行
name=Professional%20Ajax&publisher=Wiley
1 |
|
HTTP/1.1 200 OK
Date: Fri, 22 May 2009 06:07:21 GMT
Content-Type: text/html; charset=UTF-8
空行
1 |
|
在http请求接收部分,会涉及到init和read_once函数,但init仅仅是对私有成员变量进行初始化,不用过多讲解。
这里,对read_once进行介绍。read_once读取浏览器端发送来的请求报文,直到无数据可读或对方关闭连接,读取到m_read_buffer中,并更新m_read_idx。
1 | //循环读取客户数据,直到无数据可读或对方关闭连接 |
int recv(int sockfd, char * buf, int len, int flags);
- sockfd:连接的fd
- buf:用于接收数据的缓冲区
- len:缓冲区长度,一般是参数2的字节数-1,把
\0字符串结尾留出来 - flags:指定调用方式,一般设置为0
- 返回值:成功返回实际读到的字节数。如果recv在copy时出错,那么它返回err,err小于0;如果recv函数在等待协议接收数据时网络中断了,那么它返回0 。
在Linux环境下开发经常会碰到很多错误(设置errno),其中EAGAIN是其中比较常见的一个错误(比如用在非阻塞操作中)。EAGAIN和 EWOULDBLOCK等效!
- 从字面上来看,是提示再试一次。这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。例如,以O_NONBLOCK的标志打开文件/socket/FIFO,如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。又例如,当一个系统调用(比如fork)因为没有足够的资源(比如虚拟内存)而执行失败,返回EAGAIN提示其再调用一次(也许下次就能成功)。
- 这个错误不会破坏socket的同步,不用管它,下次循环接着recv就可以。对非阻塞socket而言,EAGAIN不是一种错误。在VxWorks和Windows上,EAGAIN的名字叫做EWOULDBLOCK。
struct iovec 结构体定义了一个向量元素,通常这个 iovec 结构体用于一个多元素的数组,对于每一个元素,iovec 结构体的字段 iov_base 指向一个缓冲区,这个缓冲区存放的是网络接收的数据(read),或者网络将要发送的数据(write)。iovec 结构体的字段 iov_len 存放的是接收数据的最大长度(read),或者实际写入的数据长度(write)。
struct iovec { /* Starting address (内存起始地址)*/ void *iov_base; /* Number of bytes to transfer(这块内存长度) */ size_t iov_len; };1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
* struct stat这个结构体是用来描述一个linux系统文件系统中的文件属性的结构。
* ```c++
struct stat
{
dev_t st_dev; /* ID of device containing file -文件所在设备的ID*/
ino_t st_ino; /* inode number -inode节点号*/
mode_t st_mode; /* protection -保护模式?*/
nlink_t st_nlink; /* number of hard links -链向此文件的连接数(硬连接)*/
uid_t st_uid; /* user ID of owner -user id*/
gid_t st_gid; /* group ID of owner - group id*/
dev_t st_rdev; /* device ID (if special file) -设备号,针对设备文件*/
off_t st_size; /* total size, in bytes -文件大小,字节为单位*/
blksize_t st_blksize; /* blocksize for filesystem I/O -系统块的大小*/
blkcnt_t st_blocks; /* number of blocks allocated -文件所占块数*/
time_t st_atime; /* time of last access -最近存取时间*/
time_t st_mtime; /* time of last modification -最近修改时间*/
time_t st_ctime; /* time of last status change - */
};//_stat函数用来获取指定路径的文件或者文件夹的信息。 //! 需要包含de头文件 #include <sys/types.h> #include <sys/stat.h> int stat( const char *filename //文件或者文件夹的路径 , struct stat *buf //获取的信息保存在内存中 ); //! prototype,原型 //正确——返回0 //错误——返回-1,具体错误码保存在errno中1
2
3
4
5
6
7
8
9
10
* 一般情况下,我们关心文件大小和创建时间、访问时间、修改时间。
* #### mmap
* 用于将一个文件或其他对象映射到内存,提高文件的访问速度。
* ```c++
void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
int munmap(void* start,size_t length);start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址
length:映射区的长度
prot:期望的内存保护标志,不能与文件的打开模式冲突
- PROT_READ 表示页内容可以被读取
flags:指定映射对象的类型,映射选项和映射页是否可以共享
- MAP_PRIVATE 建立一个写入时拷贝的私有映射,内存区域的写入不会影响到原文件
fd:有效的文件描述符,一般是由open()函数返回
off_toffset:被映射对象内容的起点
返回值:成功返回创建的映射区的首地址;失败返回宏MAP_FAILED。
writev
writev函数用于在一次函数调用中写多个非连续缓冲区,有时也将这该函数称为聚集写。
```c++
#include <sys/uio.h>
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);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
* filedes表示文件描述符
* iov为前述io向量机制结构体iovec
* iovcnt为结构体的个数
* 若成功则返回已写的字节数,若出错则返回-1。`writev`以顺序`iov[0]`,`iov[1]`至`iov[iovcnt-1]`从缓冲区中聚集输出数据。`writev`返回输出的字节总数,通常,它应等于所有缓冲区长度之和。
* **特别注意:** 循环调用writev时,需要重新处理iovec中的指针和长度,该函数不会对这两个成员做任何处理。writev的返回值为已写的字节数,但这个返回值“实用性”并不高,因为参数传入的是iovec数组,计量单位是iovcnt,而不是字节数,我们仍然需要通过遍历iovec来计算新的基址,另外写入数据的“结束点”可能位于一个iovec的中间某个位置,因此需要调整临界iovec的io_base和io_len。
补充:
Linux中系统调用的错误都存储于 `errno`中,`errno`由操作系统维护,存储就近发生的错误,即下一次的错误码会覆盖掉上一次的错误。
> *PS: 只有当系统调用或者调用lib函数时出错,才会置位`errno`!*
查看系统中所有的`errno`所代表的含义,可以采用如下的代码:
```c++
/* Function: obtain the errno string
* char *strerror(int errno)
*/
#include <stdio.h>
#include <string.h> //for strerror()
//#include <errno.h>
int main()
{
int tmp = 0;
for(tmp = 0; tmp <=256; tmp++)
{
printf("errno: %2d\t%s\n",tmp,strerror(tmp));
}
return 0;
}
//输出信息如下:
errno: 0 Success
errno: 1 Operation not permitted
errno: 2 No such file or directory
errno: 3 No such process
errno: 4 Interrupted system call
errno: 5 Input/output error
errno: 6 No such device or address
errno: 7 Argument list too long
errno: 8 Exec format error
errno: 9 Bad file descriptor
errno: 10 No child processes
errno: 11 Resource temporarily unavailable
errno: 12 Cannot allocate memory
errno: 13 Permission denied
errno: 14 Bad address
errno: 15 Block device required
errno: 16 Device or resource busy
errno: 17 File exists
errno: 18 Invalid cross-device link
errno: 19 No such device
errno: 20 Not a directory
errno: 21 Is a directory
errno: 22 Invalid argument
errno: 23 Too many open files in system
errno: 24 Too many open files
errno: 25 Inappropriate ioctl for device
errno: 26 Text file busy
errno: 27 File too large
errno: 28 No space left on device
errno: 29 Illegal seek
errno: 30 Read-only file system
errno: 31 Too many links
errno: 32 Broken pipe
errno: 33 Numerical argument out of domain
errno: 34 Numerical result out of range
errno: 35 Resource deadlock avoided
errno: 36 File name too long
errno: 37 No locks available
errno: 38 Function not implemented
errno: 39 Directory not empty
errno: 40 Too many levels of symbolic links
errno: 41 Unknown error 41
errno: 42 No message of desired type
errno: 43 Identifier removed
errno: 44 Channel number out of range
errno: 45 Level 2 not synchronized
errno: 46 Level 3 halted
errno: 47 Level 3 reset
errno: 48 Link number out of range
errno: 49 Protocol driver not attached
errno: 50 No CSI structure available
errno: 51 Level 2 halted
errno: 52 Invalid exchange
errno: 53 Invalid request descriptor
errno: 54 Exchange full
errno: 55 No anode
errno: 56 Invalid request code
errno: 57 Invalid slot
errno: 58 Unknown error 58
errno: 59 Bad font file format
errno: 60 Device not a stream
errno: 61 No data available
errno: 62 Timer expired
errno: 63 Out of streams resources
errno: 64 Machine is not on the network
errno: 65 Package not installed
errno: 66 Object is remote
errno: 67 Link has been severed
errno: 68 Advertise error
errno: 69 Srmount error
errno: 70 Communication error on send
errno: 71 Protocol error
errno: 72 Multihop attempted
errno: 73 RFS specific error
errno: 74 Bad message
errno: 75 Value too large for defined data type
errno: 76 Name not unique on network
errno: 77 File descriptor in bad state
errno: 78 Remote address changed
errno: 79 Can not access a needed shared library
errno: 80 Accessing a corrupted shared library
errno: 81 .lib section in a.out corrupted
errno: 82 Attempting to link in too many shared libraries
errno: 83 Cannot exec a shared library directly
errno: 84 Invalid or incomplete multibyte or wide character
errno: 85 Interrupted system call should be restarted
errno: 86 Streams pipe error
errno: 87 Too many users
errno: 88 Socket operation on non-socket
errno: 89 Destination address required
errno: 90 Message too long
errno: 91 Protocol wrong type for socket
errno: 92 Protocol not available
errno: 93 Protocol not supported
errno: 94 Socket type not supported
errno: 95 Operation not supported
errno: 96 Protocol family not supported
errno: 97 Address family not supported by protocol
errno: 98 Address already in use
errno: 99 Cannot assign requested address
errno: 100 Network is down
errno: 101 Network is unreachable
errno: 102 Network dropped connection on reset
errno: 103 Software caused connection abort
errno: 104 Connection reset by peer
errno: 105 No buffer space available
errno: 106 Transport endpoint is already connected
errno: 107 Transport endpoint is not connected
errno: 108 Cannot send after transport endpoint shutdown
errno: 109 Too many references: cannot splice
errno: 110 Connection timed out
errno: 111 Connection refused
errno: 112 Host is down
errno: 113 No route to host
errno: 114 Operation already in progress
errno: 115 Operation now in progress
errno: 116 Stale file handle
errno: 117 Structure needs cleaning
errno: 118 Not a XENIX named type file
errno: 119 No XENIX semaphores available
errno: 120 Is a named type file
errno: 121 Remote I/O error
errno: 122 Disk quota exceeded
errno: 123 No medium found
errno: 124 Wrong medium type
errno: 125 Operation canceled
errno: 126 Required key not available
errno: 127 Key has expired
errno: 128 Key has been revoked
errno: 129 Key was rejected by service
errno: 130 Owner died
errno: 131 State not recoverable
errno: 132 Operation not possible due to RF-kill
errno: 133 Memory page has hardware error
errno: 134~255 unknown error!
Linux中,在头文件 /usr/include/asm-generic/errno-base.h 对基础常用errno进行了宏定义:
1 |
在 /usr/include/asm-asm-generic/errno.h 中,对剩余的errno做了宏定义:
1 |
|
http的调用
这不是http的实现,实现后面再说,这里是使用epoll调用的运行代码
1 | //创建MAX_FD个http类对象 |
http实现
1 |
|
- MYSQL_RES *mysql_store_result(MYSQL *mysql)
- 对于成功检索了数据的每个查询(SELECT、SHOW、DESCRIBE、EXPLAIN、CHECK TABLE等),必须调用mysql_store_result()或mysql_use_result() 。
- 对于其他查询,不需要调用mysql_store_result()或mysql_use_result(),但是如果在任何情况下均调用了mysql_store_result(),它也不会导致任何伤害或性能降低。通过检查mysql_store_result()是否返回0,可检测查询是否没有结果集(以后会更多)。
- 如果希望了解查询是否应返回结果集,可使用mysql_field_count()进行检查。
- unsigned int mysql_field_count(MYSQL *mysql)
- 返回作用在连接上的最近查询的列数。
- MYSQL_FIELD *mysql_fetch_field(MYSQL_RES *result)
- 返回采用MYSQL_FIELD结构的结果集的列。重复调用该函数,以检索关于结果集中所有列的信息。
- MYSQL_ROW mysql_fetch_row(MYSQL_RES *result)
- 检索结果集的下一行。在mysql_store_result()之后使用时,如果没有要检索的行,mysql_fetch_row()返回NULL。在mysql_use_result()之后使用时,如果没有要检索的行或出现了错误,mysql_fetch_row()返回NULL。行内值的数目由mysql_num_fields(result)给出。
- int munmap(void *start,size_t length);
- 函数说明 munmap()用来取消参数start所指的映射内存起始地址,参数length则是欲取消的内存大小。当进程结束或利用exec相关函数来执行其他程序时,映射内存会自动解除,但关闭对应的文件描述词时不会解除映射。
- 返回值 如果解除映射成功则返回0,否则返回-1,错误原因存于errno中错误代码EINVAL参数 start或length 不合法。
第五站
项目中使用的是SIGALRM信号,具体的,利用alarm函数周期性地触发SIGALRM信号,信号处理函数利用管道通知主循环,主循环接收到该信号后对升序链表上所有定时器进行处理,若该段时间内没有交换数据,则将该连接关闭,释放所占用的资源。
定时器处理非活动连接模块,主要分为两部分,其一为定时方法与信号通知流程,其二为定时器及其容器设计与定时任务的处理。
定时器与信号API
1 | - Linux中的信号是一种消息处理机制,它本质上是一个整数,不同的信号对应不同的值,信号在系统中的优先级是非常高的。 |
- 还有一个信号:SIGPIPE:当服务器close一个连接时,若client端接着发数据。根据TCP 协议的规定,会收到一个RST响应,client再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不要再写了。
- TCP是全双工的信道, 可以看作两条单工信道, TCP连接两端的两个端点各负责一条. 当对端调用close时, 虽然本意是关闭整个两条信道, 但本端只是收到FIN包. 按照TCP协议的语义, 表示对端只是关闭了其所负责的那一条单工信道, 仍然可以继续接收数据. 也就是说, 因为TCP协议的限制, 一个端点无法获知对端的socket是调用了close还是shutdown.
- 对一个已经收到FIN包的socket调用read方法, 如果接收缓冲已空, 则返回0, 这就是常说的表示连接关闭. 但第一次对其调用write方法时, 如果发送缓冲没问题, 会返回正确写入(发送). 但发送的报文会导致对端发送RST报文, 因为对端的socket已经调用了close, 完全关闭, 既不发送, 也不接收数据. 所以, 第二次调用write方法(假设在收到RST之后), 会生成SIGPIPE信号, 导致进程退出.
- 为了避免进程退出, 可以捕获SIGPIPE信号, 或者忽略它, 给它设置SIG_IGN信号处理函数:signal(SIGPIPE, SIG_IGN);SIG_IGN表示忽略信号
- Linux中的每个信号产生之后都会有对应的默认处理行为,如果想要忽略某些信号或者修改某些信号的默认行为就需要在程序中捕捉该信号。
- 程序中的信号捕捉是一个注册的动作,提前告诉应用程序信号产生之后的处理动作,当进程中对应的信号产生了,这个处理动作也就被调用了。
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); // 被废弃的成员,一般不使用 };1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
* flag有以下几个:
* SA_RESTART,使被信号打断的系统调用自动重新发起
* SA_NOCLDSTOP,使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号
* SA_NOCLDWAIT,使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程
* SA_NODEFER,使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号
* SA_RESETHAND,信号处理之后重新设置为默认的处理方式
* sigaction函数:
* ```c++
int sigaction(
int signum, // 要捕捉的信号。
const struct sigaction *act, // 对信号设置新的处理方式。
struct sigaction *oldact // 上一次信号处理方式,一般指定为NULL。
);返回值,0 表示成功,-1 表示有错误发生。
sigfillset函数:
- ```c++
int sigfillset(sigset_t * set);1
2
3
4
5
6
7
8
* sigfillset()用来将参数set信号集初始化,然后把所有的信号加入到此信号集里,即将所有的信号标志位置为1,屏蔽所有的信号。信号集是在执行信号处理程序时被阻塞的信号集。因此,当执行信号处理程序时,所有信号都被阻塞,不必担心另一个信号会中断信号处理程序。
* SIGALRM、SIGTERM信号,是整形数
* ```c++
#define SIGALRM 14 //由alarm系统调用产生timer时钟信号
#define SIGTERM 15 //终端发送的终止信号
- ```c++
alarm函数
#include<unistd.h> unsigned int alarm(unsigned int seconds);* domain表示协议族,PF_UNIX或者AF_UNIX,AF = Address Family、PF = Protocol Family。PF_UNIX (也称作 PF_LOCAL ) 套接字族用来在同一机器上的提供有效的进程间通讯。AF\_和PF\_的值直接可以替换,没有其它区别。 * type表示协议,可以是SOCK_STREAM或者SOCK_DGRAM,SOCK_STREAM基于TCP,SOCK_DGRAM基于UDP * protocol表示类型,只能为0 * sv[2]表示套节字柄对,该两个句柄作用相同,均能进行读写双向操作1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
* alarm也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间到时,它向进程发送SIGALRM信号。可以设置忽略或者不捕获此信号,如果采用默认方式其动作是终止调用该alarm函数的进程。
* 要注意的是,一个进程只能有一个闹钟时间,如果在调用alarm之前已设置过闹钟时间,则任何以前的闹钟时间都被新值所代替。需要注意的是,经过指定的秒数后,信号由内核产生,由于进程调度的延迟,所以进程得到控制从而能够处理该信号还需要一些时间。
* 返回值:成功:如果调用此alarm()前,进程已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。
* socketpair函数
* 在linux下,使用socketpair函数能够创建一对套接字进行通信,项目中使用管道通信。socketpair创建的描述符任意一端既可以读也可以写。
* ```c++
#include <sys/types.h>
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int sv[2]);返回结果, 0为创建成功,-1为创建失败
send函数,当套接字发送缓冲区变满时,send通常会阻塞,除非套接字设置为非阻塞模式,当缓冲区变满时,返回EAGAIN或者EWOULDBLOCK错误,此时可以调用select函数来监视何时可以发送数据。
```c++
#include <sys/types.h>
#include <sys/socket.h>ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);
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
* sockfd:指定发送端套接字描述符。
* buff: 存放要发送数据的缓冲区
* nbytes: 实际要改善的数据的字节数
* flags: 一般设置为0
* 1) send先比较发送数据的长度nbytes和套接字sockfd的发送缓冲区的长度,如果nbytes > 套接字sockfd的发送缓冲区的长度, 该函数返回SOCKET_ERROR。
2) 如果nbtyes <= 套接字sockfd的发送缓冲区的长度,那么send先检查协议是否正在发送sockfd的发送缓冲区中的数据,如果是就等待协议把数据发送完,如果协议还没有开始发送sockfd的发送缓冲区中的数据或者sockfd的发送缓冲区中没有数据,那么send就比较sockfd的发送缓冲区的剩余空间和nbytes。
3) 如果 nbytes > 套接字sockfd的发送缓冲区剩余空间的长度,send就一起等待协议把套接字sockfd的发送缓冲区中的数据发送完。
4) 如果 nbytes < 套接字sockfd的发送缓冲区剩余空间大小,send就仅仅把buf中的数据copy到剩余空间里(注意:并不是send把套接字sockfd的发送缓冲区中的数据传到连接的另一端的,而是协议传送的。send仅仅是把buf中的数据copy到套接字sockfd的发送缓冲区的剩余空间里)。
5) 如果send函数copy成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果在等待协议传送数据时网络断开,send函数也返回SOCKET_ERROR。
6) 如果send函数copy成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果在等待协议传送数据时网络断开,send函数也返回SOCKET_ERROR。
7) 在unix系统下,如果send在等待协议传送数据时网络断开,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的处理是进程终止。
## 头文件
lst_timer.h
```c++
#ifndef LST_TIMER
#define LST_TIMER
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <stdarg.h>
#include <errno.h>
#include <sys/wait.h>
#include <sys/uio.h>
#include <time.h>
#include "../log/log.h"
//连接资源结构体成员需要用到定时器类
//前向声明
class util_timer;
//连接资源
struct client_data
{
//客户端socket地址,项目中未使用
sockaddr_in address;
int sockfd;//socket文件描述符
util_timer *timer;//定时器
};
//定时器类
class util_timer
{
public:
util_timer() : prev(NULL), next(NULL) {}
public:
time_t expire;//超时时间
void (* cb_func)(client_data *);//回调函数指针,这个回调函数会删除client_data的资源连接
client_data *user_data;//连接资源,嵌套类使用指针,相当于内部成员指针互相指向对方实例
util_timer *prev;//前向定时器
util_timer *next;//后继定时器
};
//定时器容器类,双向链表
class sort_timer_lst
{
public:
sort_timer_lst();
~sort_timer_lst();//常规销毁链表
void add_timer(util_timer *timer);//添加定时器,内部调用私有成员add_timer
void adjust_timer(util_timer *timer);//调整定时器,任务发生变化时,调整定时器在链表中的位置
void del_timer(util_timer *timer);//删除定时器
void tick();//定时任务处理函数,SIGALRM信号每次被触发,主循环中调用一次定时任务处理函数,处理链表容器中到期的定时器。
private:
void add_timer(util_timer *timer, util_timer *lst_head);
//创建头尾指针,方便管理
util_timer *head;
util_timer *tail;
};
class Utils//资源管理类
{
public:
Utils() {}
~Utils() {}
void init(int timeslot);
//对文件描述符设置非阻塞
int setnonblocking(int fd);
//将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT
void addfd(int epollfd, int fd, bool one_shot, int TRIGMode);
//信号处理函数
static void sig_handler(int sig);
//设置信号函数
void addsig(int sig, void(handler)(int), bool restart = true);
//定时处理任务,重新定时以不断触发SIGALRM信号
void timer_handler();
void show_error(int connfd, const char *info);
public:
static int *u_pipefd;//管道描述符
sort_timer_lst m_timer_lst;//定时器容器
static int u_epollfd;//事务描述符
int m_TIMESLOT;
};
void cb_func(client_data *user_data);//回调函数
#endif
.cpp实现
1 |
|
使用逻辑
首先注册(设置)好信号,比如SIGALRM信号,这使得产生这个信号时,能有对应的方式和处理(默认是终止进程),这里的方式是restart、屏蔽其他信号,处理是通过管道向主循环发这个信号。产生的方式是alarm(),主循环中每次尝试从管道获取信号,如果有这个信号,则设置timeout为true说明有超时事件要处理,因为是非必须事件,在这轮循环读写完再进行处理。
处理会调用timer_handler(),首先调用tick(),把定时器超时的都关了(调用cb_func),然后重新alarm()。
关于SIGTERM:程序结束信号,kill或Ctrl+C触发,默认终止进程。
也就是:
信号
1.先知道有些动作会产生一些信号
2.设置(注册)这些信号产生后的动作——方式(flag)和处理函数(handler)
3.处理函数只是通知主循环有个信号产生,主循环要做对应的处理。
主循环,我们看看会发生什么
- 1.当一个连接到来时,要创建一个定时器给它,初始化时间和回调函数等变量;
- 2.如果连接有读写,更新时间;
- 3.无论有没有定时器超时,每隔timeslot(时隙)会alarm一次,触发信号后主循环得知信号产生,去查看有哪些连接的定时器超时了,超时就关闭连接,然后重新设置alarm,循环往复。
1 | //定时处理任务,重新定时以不断触发SIGALRM信号 |
第六站
内容比较少
config
头文件config.h
1 |
|
1 |
|
getopt() 方法是用来分析命令行参数的,该方法由 Unix 标准库提供,包含在 <unistd.h> 头文件中。
```c++
int getopt(int argc, char * const argv[], const char *optstring);
extern char *optarg; //选项的参数指针
extern int optind, //下一次调用getopt的时,从optind存储的位置处重新开始检查选项。
extern int opterr, //当opterr=0时,getopt不向stderr输出错误信息。
extern int optopt; //当命令行选项字符不包括在optstring中或者选项缺少必要的参数时,该选项存储在optopt中,getopt返回’?’、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
* argc:通常由函数直接传入,表示参数的数量
* argv:通常也由函数直接传入,表示参数的字符串变量数组
* optstring:一个包含正确的参数选项字符串,用于参数的解析。例如 “abc:”,其中 -a,-b 就表示两个普通选项,-c 表示一个必须有参数的选项,因为它后面有一个冒号
* 1.单个字符,表示选项,
* 2.单个字符后接一个冒号:表示该选项后必须跟一个参数。参数紧跟在选项后或者以空格隔开。该参数的指针赋给optarg。
* 3 单个字符后跟两个冒号,表示该选项后必须跟一个参数。参数必须紧跟在选项后不能以空格隔开。该参数的指针赋给optarg。(这个特性是GNU的扩张)。
* getopt处理以'-’开头的命令行参数,如optstring="ab:c::d::",命令行为getopt.exe -a -b host -ckeke -d haha
在这个命令行参数中,-a和-h就是选项元素,去掉'-',a,b,c就是选项。host是b的参数,keke是c的参数。但haha并不是d的参数,因为它们中间有空格隔开。
* getopt()用来分析命令行参数。参数argc和argv是由main()传递的参数个数和内容。参数optstring 则代表欲处理的选项字符串。此函数会返回在argv 中下一个的选项字母(指针不断移动),此字母会对应参数optstring 中的字母。如果选项字符串里的字母后接着冒号“:”,则表示还有相关的参数,全域变量optarg 即会指向此额外参数。如果getopt()找不到符合的参数则会印出错信息,并将全域变量optopt设为“?”字符,如果不希望getopt()印出错信息,则只要将全域变量opterr设为0即可。
* 还要注意的是默认情况下getopt会重新排列命令行参数的顺序,所以到最后所有不包含选项的命令行参数都排到最后。
如getopt.exe -a ima -b host -ckeke -d haha, 都最后命令行参数的顺序是: -a -b host -ckeke -d ima haha
关于proactor模式,小林coding的这篇分析写得很好,推荐看一看:[如何深刻理解Reactor和Proactor? - 知乎 (zhihu.com)](https://www.zhihu.com/question/26943938)
项目中用的是假reactor和模拟proactor(同步的):[(29条消息) 两种高效的事件处理模式:Reactor模式和Proactor模式_ZY-JIMMY的博客-CSDN博客_reactor模式和proactor](https://blog.csdn.net/ZYZMZM_/article/details/98049471)
## main
main.cpp
```c++
#include "config.h"
int main(int argc, char *argv[])
{
//需要修改的数据库信息,登录名,密码,库名
string user = "root";
string passwd = "root";
string databasename = "qgydb";
//命令行解析
Config config;
config.parse_arg(argc, argv);
WebServer server;//websever在config.h中导入了websever.h
//初始化
server.init(config.PORT, user, passwd, databasename, config.LOGWrite,
config.OPT_LINGER, config.TRIGMode, config.sql_num, config.thread_num,
config.close_log, config.actor_model);
//日志
server.log_write();
//数据库
server.sql_pool();
//线程池
server.thread_pool();
//触发模式
server.trig_mode();
//监听
server.eventListen();
//运行
server.eventLoop();
return 0;
}
makefile&g++
先看这个入门:(29条消息) Makefile 语法入门_阿飞__的博客-CSDN博客_makefile语法
再看:(29条消息) Makefile文件语法规则及用法总结_fangye945a的博客-CSDN博客_makefile语法规则
Makefile 条件判断 - ifeq、ifneq、ifdef、ifndef - Makefile 简明教程 | 宅学部落 (zhaixue.cc)
g++参数:C/C++专题—gcc g++ 参数详解 - 知乎 (zhihu.com)
rm命令:Linux rm命令:删除文件或目录 (biancheng.net)
项目中的makefile:
1 | CXX ?= g++ |
第七站
顶层实现
头文件websever.h
1 |
|
.cpp实现
1 |
|
int socket(int af, int type, int protocol);1
2
3
4
5
6
7
8
9
10
11
12
* af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。
* type 为数据传输方式/套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字)。
* protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。
* 为什么还需要第三个参数呢?一般情况下有了 af 和 type 两个参数就可以创建套接字了,操作系统会自动推演出协议类型,除非遇到这样的情况:有两种不同的协议支持同一种地址类型和数据传输类型。如果我们不指明使用哪种协议,操作系统是没办法自动推演的。
* 若无错误发生,socket()返回引用新套接口的描述字。
* ```c++
int listen(int sockfd, int backlog);listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
成功返回0
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);1
2
3
4
5
* connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。
* ```c++
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为客户端协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。
struct linger用法:
Linux下tcp连接断开的时候调用close()函数,有优雅断开和强制断开两种方式。那么如何设置断开连接的方式呢?是通过设置socket描述符一个linger结构体属性。
```c++
#include <arpa/inet.h>struct linger {
int l_onoff;
int l_linger;
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
* 三种断开方式:
* 1. l_onoff = 0; l_linger忽略:close()立刻返回,底层会将未发送完的数据发送完成后再释放资源,即优雅退出。
2. l_onoff != 0; l_linger = 0:close()立刻返回,但不会发送未发送完成的数据,而是通过一个REST包强制的关闭socket描述符,即强制退出。
3. l_onoff != 0; l_linger > 0:close()不会立刻返回,内核会延迟一段时间,这个时间就由l_linger的值来决定。如果超时时间到达之前,发送完未发送的数据(包括FIN包)并得到另一端的确认,close()会返回正确,socket描述符优雅性退出。否则,close()会直接返回错误值,未发送数据丢失,socket描述符被强制性退出。需要注意的时,如果socket描述符被设置为非堵塞型,则close()会直接返回值。
-----
使用完linger之后,就用setsockopt()设置
```c++
#include <sys/socket.h>
int setsockopt( int socket, int level, int option_name,const void *option_value, size_t ,ption_len);
第一个参数socket是套接字描述符。第二个参数level是被设置的选项的级别,如果想要在套接字级别上设置选项,就必须把level设置为 SOL_SOCKET。 option_name指定准备设置的选项,len是选项的长度。option_name可以有哪些取值,这取决于level,以linux 2.6内核为例(在不同的平台上,这种关系可能会有不同),在套接字级别上(SOL_SOCKET),option_name可以有以下取值:
SO_LINGER,如果选择此选项, close或 shutdown将等到所有套接字里排队的消息成功发送或到达延迟时间后>才会返回. 否则, 调用将立即返回。该选项的参数(option_value)是一个linger结构:
1
2
3
4
5
6
7
8struct linger {
int l_onoff;
int l_linger;
};
/*
如果linger.l_onoff值为0(关闭),则清 sock->sk->sk_flag中的SOCK_LINGER位;
否则,置该位,并赋sk->sk_lingertime值为 linger.l_linger。
*/SO_DEBUG,打开或关闭调试信息。当option_value不等于0时,打开调试信息,否则,关闭调试信息。它实际所做的工作是在sock->sk->sk_flag中置 SOCK_DBG(第10)位,或清SOCK_DBG位。
SO_REUSEADDR,打开或关闭地址复用功能。当option_value不等于0时,打开,否则,关闭。它实际所做的工作是置sock->sk->sk_reuse为1或0。
- SO_REUSEADDR是一个很有用的选项,一般服务器的监听socket都应该打开它。它的大意是允许服务器bind一个地址,即使这个地址当前已经存在已建立的连接,比如:
服务器启动后,有客户端连接并已建立,如果服务器主动关闭,那么和客户端的连接会处于TIME_WAIT状态,此时再次启动服务器,就会bind不成功,报:Address already in use。
服务器父进程监听客户端,当和客户端建立链接后,fork一个子进程专门处理客户端的请求,如果父进程停止,因为子进程还和客户端有连接,所以再次启动父进程,也会报Address already in use。
- SO_REUSEADDR是一个很有用的选项,一般服务器的监听socket都应该打开它。它的大意是允许服务器bind一个地址,即使这个地址当前已经存在已建立的连接,比如:
SO_DONTROUTE,打开或关闭路由查找功能。当option_value不等于0时,打开,否则,关闭。它实际所做的工作是在sock->sk->sk_flag中置或清SOCK_LOCALROUTE位。
SO_BROADCAST,允许或禁止发送广播数据。当option_value不等于0时,允许,否则,禁止。它实际所做的工作是在sock->sk->sk_flag中置或清SOCK_BROADCAST位。
等等…太多了。
sockaddr_in
struct sockaddr_in { __uint8_t sin_len; sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; char sin_zero[8]; };1
2
3
4
5
6
7
8
9
10
11
12
13
14
* sin_family指代协议族,在socket编程中只能是AF_INET
* sin_port存储端口号(使用网络字节顺序)
* sin_addr存储IP地址,使用in_addr这个数据结构
* sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节
* addr.sin_len=sizeof(addr);//socket字节长度
* sockaddr_in 结构体:struct sockaddr_in中的in 表示internet,就是网络地址,这只是我们比较常用的地址结构,属于AF_INET地址族,非常地常用
* sin_zero 初始值应该使用函数 bzero() 来全部置零。一般采用下面语句
* ```c++
struct sockaddr_in cliaddr;
bzero(&cliaddr,sizeof(cliaddr));sockaddr_in结构体变量的基本配置
- ```c++
struct sockaddr_in ina;
bzero(&ina,sizeof(ina));
ina.sin_family=AF_INET;
ina.sin_port=htons(23);
ina.sin_addr.s_addr = inet_addr(“132.241.5.10”);1
2
3
4
5
6
7
8
9
10
* sockaddr
* ```c++
struct sockaddr {
unsigned short sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14字节,包含目标地址和端口信息 */
};
- ```c++
sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了
sockaddr_in和sockaddr二者长度一样,都是16个字节,即占用的内存大小是一致的,因此可以互相转化。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。
sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息,是一种通用的套接字地址。
sockaddr_in 是internet环境下套接字的地址形式。所以在网络编程中我们会对sockaddr_in结构体进行操作,使用sockaddr_in来建立所需的信息,最后使用类型转化就可以了。一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数:sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。
- INADDR_ANY
- 转换过来就是0.0.0.0,泛指本机的意思,也就是表示本机的所有IP,因为有些机子不止一块网卡,多网卡的情况下,这个就表示所有网卡ip地址的意思。
比如一台电脑有3块网卡,分别连接三个网络,那么这台电脑就有3个ip地址了,如果某个应用程序需要监听某个端口,那他要监听哪个网卡地址的端口呢? - 如果绑定某个具体的ip地址,你只能监听你所设置的ip地址所在的网卡的端口,其它两块网卡无法监听端口,如果我需要三个网卡都监听,那就需要绑定3个ip,也就等于需要管理3个套接字进行数据交换,这样岂不是很繁琐?
- 所以出现INADDR_ANY,你只需绑定INADDR_ANY,管理一个套接字就行,不管数据是从哪个网卡过来的,只要是绑定的端口号过来的数据,都可以接收到。
bind:服务端用于将把用于通信的地址和端口绑定到 socket上。
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);- 参数 sockfd ,需要绑定的socket。
- 参数 addr ,存放了服务端用于通信的地址和端口。ip地址和端口号是放在 socketaddr_in 结构体里面的。
- 参数 addrlen ,表示 addr 结构体的大小。
返回值:成功则返回0 ,失败返回-1,错误原因存于 errno 中。如果绑定的地址错误,或者端口已被占用,bind 函数一定会报错,否则一般不会返回错误
压力测试
先安装依赖

然后到webbench-1.5的目录下:make clean,再make。
测试:
