🐱作者:一只大喵咪1201
🐱专栏:《网络》
🔥格言:你只管努力,剩下的交给时间!
上篇文章中本喵介绍了UDP网络通信的socket代码,今天介绍TCP网络通信的socket代码。
TCP
- 🧁TCP网络编程
- 🍦服务端实现
- 将套接字设置为listen状态
- 🍦客户端实现
- 🍦多进程版本
- 🍦多线程版本
- 🍦线程池版本
- 🧁日志功能
- 🧁守护进程
- 🍦变成守护进程
- 🧁总结
🧁TCP网络编程
🍦服务端实现
和udp
的网络通信一样,tcp
通信也需要服务器指定端口号,IP地址同样使用0.0.0.0
,以便客户端所有对服务器的网络请求都能收到。
class tcpServe
{
public:
// 构造函数
tcpServe(const uint16_t &port)
: _port(port), _listensock(-1)
{}
// 服务器初始化
void initServer()
{}
// 服务器运行
void start()
{}
// 析构函数
~tcpServe()
{}
private:
int _listensock; // 不是用来进行数据通信的,是用来监听链接到来的,获取新连接
uint16_t _port;
};
服务器类的框架如上面代码所示,包括一个文件描述符istensock
,还有一个端口号_port
,这里本喵没有写IP地址的成员变量,因为会给它一个缺省值,所以没有必要,后面本喵会详细讲解。
下面就是将tcpServe
类中成员函数的具体逻辑写出来了。
initServer()
首先是创建套接字,和udp
一样,使用系统调用socket
,只是填的参数不一样,第一个参数仍然是AF_INET
表示网络间通信,第二个参数这里使用的是SOCK_STREAN
,表示面向字节流的,不再是面向用户数据报了,第三个参数仍然是0。
创建套接字的结果使用日志函数logMessage()
来记录日志。
然后就是将端口号和IP地址使用系统调用bind
和操作系统绑定,填充的结构体仍然是struct sockaddr_in
,和之前的udp
一样,只是在填充IP地址的时候,本喵这里使用了INADDR_ANY
,它的意义就是0.0.0.0
。
它在Linux中的定义如上图所示,我们使用的0.0.0.0
转换成uint16_t
以后就是0x00000000
。
在使用bind
进行绑定的时候,同样需要将填充的结构体强转成struct sockaddr*
,绑定的结果使用日志来记录。
将套接字设置为listen状态
第三步就是将套接字设置为监听状态,使用系统调用listen
,如上图所示。
系统调用listen
的作用是将套接字设置为监听状态,此时这个套接字和udp
中的套接字不一样,它不用来通信,只用来检测客户端的连接请求的。
第一个参数sockfd
就是使用socket
创建套接字返回的文件描述符,第二个参数这里本喵暂时不做讲解,等后面会详细讲解,这里随便给一个值就可以,不要太大,本喵这里给的是5。
设置成功返回0,设置失败返回-1,并且设置错误码,同样使用日志来记录设置结果。
以上三步就是初始化initServe
中的内容。
start()
第四步写在start
函数中,如上图所示,使用accept
来接收客户端的连接请求,有点像udp
中的recvfrom
一样,只是accept
是用来接收套接字的连接请求,而recvfrom
是接收套接字中的数据的。
accept
系统调用的参数和recvfrom
中的一样,如上图所示,accept
的作用就是接收来自套接字中的连接请求,也就是来自客户端的连接请求。
返回值:
上面本喵说过,设置为listen
状态的套接字不用了通信,只是用来接收客户端的网络请求,具体体现在accept
的返回值上。
此时第一步中创建的套接字就像是一个门童,使用accept
来接收客户端的连接请求,如果有连接请求并且接收成功,那么会返回一个文件描述符fd
。
这里的文件描述符sock
和前面的_listensock
不是一个东西,_listensock
是我们创建的,是专门用来接收连接请求的,而accept
返回的sock
是操作系统在接收成功连接请求后新创建的套接字的文件描述符。
sock
指向的文件描述符是服务端专门用来和客户端通信的,所以每有一个客户端向服务器发起连接请求,客户端接收成功够都会创建一个套接字用来一对一的提供服务。
- _listensock:我们创建套接字返回的文件描述符,必须设置为
listen
状态,专门用来检测客户端的连接请求的。- sock:
accept
返回的文件描述符,是操作系统自动创建的套接字的文件描述符,该套接字专门用来和客户端进行一对一网络通信的。
如果accept
接收连接请求失败,则返回-1,并且设置错误码。这里的失败并不是致命的,就像门童拉客一样,拉客失败也没有什么,继续进行下一次拉客就行。
所以accept
失败也没有什么,继续接收下一个连接请求即可,所以本喵在代码中,如果接收失败,使用了continue
继续接收连接请求。
服务器的服务函数:
至此,进行tcp网络通信的所有准备工作已经做完,接下来就是进行具体的服务了,也就是读取客户端发送来的数据并做相应的处理了。
如上图代码所示,就是服务器指向的具体服务函数。
客户端读取客户端发送来的数据时,是从accept
返回的文件描述符sock
指向的套接字中读取数据的,因为这个套接字是专门用来服务客户端的。
- 读取数据时,使用的是
read
系统调用,和读取普通文件一模一样。
数据读取成功后,做一些处理,先将读取的数据打印一下,然后加一个serve[echo]
回显,再给客户端发送过去。
- 发送数据时,使用的是
write
系统调用,写入的也是sock
指向的套接字,同样与向普通文件中写入数据一模一样。
在读取普通文件的时候,如果文件被读完了,read
会返回0,表示文件的内容被读取完毕。
但是在使用read
读取tcp套接字的时候,如果读取到0,表示客户端关闭了它的套接字,代表着客户端不再进行网络通信了,此时服务端就可以结束这次通信了,也就是将sock
指向的套接字关闭。
以上代码都是在tcpServe.hpp
中写的,都是tcpServe
类中的成员函数以及成员变量,它是对服务器的一个抽象表示。
tcpServe.cpp
同样使用智能指针unique_ptr
来管理这个服务器,和udp
网络通信一样。
运行服务器以后,通过日志信息可以看到,套接字创建成功,bind
成功,并且成功将套接字设置成了listen
状态。
之后服务器就阻塞不动了,此时它应该执行的是accept
函数,也就是应该正在接收来自客户端的网络请求。但是它此时阻塞不动了,这是因为目前没有网络连接请求到来。
accept
是阻塞执行的,在没有网络连接请求的时候,会阻塞等待,直到客户端的网络连接请求到来。
使用指令netstat -nltp
可以查看当前机器上的tcp网络通信进程,如上图所示。
绿色框中的就是我们前面写的tcp服务器进程,其中IP地址是0.0.0.0
,如上图蓝色框中所示,端口号是8080,还可以看到进程的pid
是22099
,进程名字是./tcpserve
,说明我们写的tcp服务器没有问题,成功的运行了起来。
🍦客户端实现
class tcpClient
{
public:
// 构造函数
tcpClinet(const string& serverip, const uint16_t serverport)
:_serverip(serverip), _serverport(serverport), _sockfd(-1)
{}
// 客户端初始化
void initClient()
{}
// 客户端启动
void start()
{}
// 析构函数
~tcpClinet()
{}
private:
string _serverip;
uint16_t _serverport;
int _sockfd;
};
客户端需要的成员变量和udp
中一样,也是需要服务端的IP地址,服务端的端口号,以及客户端自己创建套接字的文件描述符_sockfd
。成员函数和服务端类似,接下来要做的就是实现具体的逻辑。
initClient()
首先创建套接字,如果失败直接打印错误信息,客户端没有使用日志来记录。和udp
一样,tcp
通信的客户端也不需要显式绑定,同样也是由系统自动去绑定。
start()
第三步要发起连接,使用的系统调用是connect
。
用法和recfrom
以及accept
非常类似,第一个参数是创建的套接字的文件描述符,第二个参数是填充的struct sockaddr_in
结构体,在传参的时候,需要强转为struct sockaddr*
类型,第三个参数是结构体的大小。发起请求成功返回0,失败返回-1。
用法非常熟悉,只是作用不同,connect
的作用就是在套接字中发起一个网络请求,这个网络请求服务端的监听套接字可以检测到。
请求发起成功够就开始通信了,同样使用系统调用read
和write
向套接字中读取和写入数据,和操作普通文件一样。
这个客户端程序中,先从标准输入获取用户从键盘上输入的数据,然后通过write
写入到套接字中,也就是发给服务器,然后使用read
从套接字中读取服务端回显的信息并且打印出来。
以上就是客户端的代码,同样放在一个类中,在使用的时候直接实例化后就可以使用。
tcpClient.cpp
同样使用智能指针来管理客户端对象,具体的不再讲解。
服务端进程跑起来后,创建套接字,绑定以及将套接字设置成监听状态,当这里准备工作做完以后,在接收连接请求的时候会阻塞等待连接请求的到来。
在客户端进程跑起来后,服务端立刻接收到了连接请求,并且创建了一个套接字用户服务客户端,此时客户端和服务端就可以进行网络通信了。
客户端发送什么,服务端就收到什么,然后再经过加工将数据返回给客户端。
使用指令netstat -anlp
可以查看当前机器上运行的网络通信进程,如上图所示。
- 红色框中的是服务器进程,本地环回IP地址是
127.0.0.1
,端口号是8080
,与其连接的客户端端口号是40428
。 - 绿色框中的是客户端进程,本地环回IP地址仍然是
127.0.0.1
,端口号是40428
,与其连接的服务器端口号是8080
。
由于本喵是在一台机器上测试的,所以查看网络通信进程的时候,可以同时看到服务器的和客户端的。
在服务器运行起来后,第一个客户端可以成功建立连接,第二个客户端就无法连接了,处于阻塞等待状态。
这是因为第一个客户端连接连接后,服务器就陷入了死循环,如上图代码所示。
在start
中有一个while(1)
,用来不断接收接收来自客户端的连接请求,连接成功后进入具体的服务函数serviceIO
,在这个函数中也有一个while(1)
循环,不断进行客户端和服务端的网络IO。
所以当第一个客户端连接成功后,服务器就陷入了serviceIO
的死循环中,当新的客户端发起连接请求时,服务器无法accept
到,所以表现出来的就是上图所示的现象。
这是本喵为了测试而写的测试代码。真正的tcp
网络通信中肯定不能这样,而是要满足多个客户端都能和服务端连接成功。
🍦多进程版本
为了实现多个客户端都能和服务器建立连接,第一种策略就是采用多进程的形式,服务器每建立一个和客户端的连接请求就创建一个新的进程,用来一对一服务客户端。
在start
函数中,accept
连接后,开始进行具体的服务,也就是调用serviceIO
函数,之前是直接调用,此时需要用fork
创建一个子进程,让子进程去调用serviecIO
函数,如上图绿色框中所示。
- 子进程会继承父进程的一切,所以父进程中监听套接字的文件描述符
_listensock
也会被继承下来- 为了防止子进程进行误操作以及一定程度上节省资源,在子进程中将
_listnesock
指向的监听套接字关闭。
为了避免子进程造成内存泄漏,当子进程退出以后,父进程需要回收子进程资源,可以使用waitpid
来回收子进程,有阻塞等待和非阻塞等待,这里本喵采用阻塞等待,如上图第二个红色框。
但是子进程执行的serviceIO
是一个死循环,如果子进程没有退出的话,父进程就会一直阻塞在waitpid
处等待,当有新的客户端请求连接时同样无法accept
到,就变成和之前的一样了。
- 使用
waitpid
等待子进程退出时,一般不建议使用非阻塞的方式。
为了解决这个问题,在子进程中再次创建子进程,如上图蓝色框中所示,子进程fork
后,又创建出一个子进程,该子进程是上一层父进程的孙子进程。
孙子进程一经创建,它的父进程就退出,父进程之后的代码也就是servicIO
由孙子进程来执行,孙子进程执行完服务函数后,关闭当前套接字,并且退出,此时祖父进程就可以继续循环accept
了。
- 这里将原本父进程要执行的代码转交给孙子进程去执行,祖父进程从容获得自己,可以继续去
accept
。
可以看到,此时两个客户端就可以同时和服务器建立连接,并且进行通信了,而且两个客户端之间互不影响,因为在服务端,每个客户端都存在一个套接字进行一对一服务。
使用指令ps ajx
查看当前机器上的进程时,可以看到名字为tcpserve
的进程有3个,第一个进程就是祖父进程,pid为15959
。
后面两个进程是孙子进程,每个进程对应着一个客户端,它们各自的pid值不一样,但是ppid都是1
,表示操作系统。
- 由于孙子进程的父进程退出了,所以两个孙子进程变成了孤儿进程由操作系统领养,它两的资源也由操作系统负责回收。
此时就可以实现多客户端和服务器之间的网络通信了,每多一个客户端就会在服务器上多创建一个进程,和一个套接字用来专门进行一对一服务。
但是多进程版本仍然存在缺陷,每创建一个进程的开销是很大的,需要创建并维护进程地址空间,页表,以及相应的物理内存。
🍦多线程版本
将多进程改变成多线程就能在一定程度上解决资源消耗大的问题,因为从线程和主线程是共用一份进程地址空间以及页表的。
将原本的多进程代码改成如上图所示的多线程代码。
在使用pthread_create
创建多线程的时候,传入的可执行任务不能有this
指针,但是调用的具体服务函数serviceIO
需要this
指针。
- 创建
ThreadData
用来存放新线程需要的参数
如上图所示,结构体中存在用于进行通信套接字的文件描述符_sock
,以及服务器对象tcpserve
的指针_self
。
- 新线程执行的任务
threadRoutine
使用static
修饰。
从线程执行完后的线程资源同样需要回收,如果在主线程使用pthread_join
等待回收资源的话,同样会造成主线程无法accept
。
所以在从线程中,首先就是使用pthread_detach
分离,让操作系统来回收该线程的资源,主线程可以继续去accept
新的连接请求。
在threadRoutine
中通过服务器对象的_self
指针来调用serveicIO
,此时就可以多个客户端向服务器发起连接请求了。
可以看到,结果和多进程版本一样,多个客户端可以同时和服务器进行通信,每个客户端在服务器中对应一个线程和一个套接字。
通过指令ps -aL
来查看当前机器上的线程,可以看到此时存在3个名字为tcpserve
的线程。
- 其中PID和LWP相同的是主线程,是用来
accept
新客户端的网络连接请求的。- PID和LWP不同的是两个从线程,是专门用来用来给客户端提供一对一服务的。
此时每有一个新的客户端发起连接请求,服务器就会新创建一个线程。但是这样同样也有缺陷,在创建线程的时候同样会存在很大的系统开销。
🍦线程池版本
最好的方式就是使用线程池,预先创建一批线程,每有一个客户端发起连接请求,就派一个线程去处理。
- 线程池就直接使用前面本喵写过的基于环形队列的线程池,不再讲解线程池。
在服务器accept
之前,将线程池运行起来,如上图红色框中所示,当服务器accept
新的连接请求后,将系统创建的用于服务的套接字文件描述符和serviceIO
任务推送到线程池中,让线程池分配线程去执行任务。
Task.hpp
将Task
类稍作修改,成员变量包含用于通信的套接字文件描述符和回调函数。
- 这里本喵将
serviceIO
放在了Task.hpp
中,不再是一个类成员函数。
此时将服务器运行起来以后,除了创建套接字等准备工作外,还有线程池中会起来10个线程,这10个线程就是专门用来服务客户端的。
两个客户端发送的信息服务端都能收到并且给对应的回显,而且互不影响,和前面多进程和多线程效果一样。
可以看到,此时服务器上存在10个从线程,主线程仍然在不停的accept
,每有一个客户端发起连接请求时,线程池就会安排一个线程去服务,而不会再创建新的线程。
🧁日志功能
之前本喵在调用系统调用或者库函数等有返回值的函数时,会根据返回值打印一些信息来表示该感受的调用结果,有时甚至会使用错误码。
今天本喵来介绍一下日志,日志就是专门用来记录程序的执行信息的,这部分代码放在log.hpp
中,同样可以作为一个小组件。
函数logMessage
就是用来记录日志的,其中level
表示日志等级,message
是传入的具体日志信息。
日志等级也就是程序运行过程中结果的重要程度:
- DEBUG:表示调试信息,这是程序员在调试代码时看的。
- NORMAL:表示正常信息,就是代码的运行结果是符合预期的。
- WARING:表示警告信息,存在问题但只是警告,可以暂不处理。
- ERROR:表示错误信息,代表代码运行出现了错误,需要及时处理。
- FATAL:表示致命信息,代表代码出现了致命错误,无法运行下去了。
这些等级是我们人为划分的,不同等级对应的处理方式也是由我们自己控制的,比如发生ERROR
时,是结束程序还是继续运行,都是由我们自己控制的。
这是我本喵前面代码使用的日志记录函数,真正的日志记录肯定不可能这么简单。
如上图所示,真正的日志函数采用的是可变参数,如上图所示代码。
- 注意:C语言的可变参数和可变模板参数不一样。
logMessage
函数的第三个参数...
就表示可变参数,本质上是占位符,它的参数个数以及类型都是可以变化的。
参数列表的构成:
- 参数分为两个部分:固定参数和可变参数,至少有一个固定参数,声明和普通函数一样。
实现原理:
C语言中使用 va_list 系列变参宏实现变参函数,此处va意为variable-argument(可变参数)。
typedef char * va_list;
// 把 n 圆整到 sizeof(int) 的倍数
#define _INTSIZEOF(n) ( (sizeof(n)+sizeof(int)-1) & ~(sizeof(int)-1) )
// 初始化 ap 指针,使其指向第一个可变参数。v 是变参列表的前一个参数
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
// 该宏返回当前变参值,并使 ap 指向列表中的下个变参
#define va_arg(ap, type) ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) )
// /将指针 ap 置为无效,结束变参的获取
#define va_end(ap) ( ap = (va_list)0 )
这是stdarg.h
头文件中的定义,可以看到和可变参数有关的都是宏。
va_list
本质上就是一个char*
类型的指针,指向的变量大小是一个字节。
- _INTSIZEOF(n):_INTSIZEOF进行内存地址对齐,按照sizeof(int)即栈粒度对齐,参数在内存中的地址均为sizeof(int)=4的倍数。例如,若1≤sizeof(n)≤4,则_INTSIZEOF(n)=4;若5≤sizeof(n)≤8,则_INTSIZEOF(n)=8。
- va_start(ap,v):根据
va_list
中的参数v
在栈中的内存地址,加上_INTSIZEOF(v)占内存大小后,使ap
指向v
的下一个参数。用这个宏初始化 ap 指针,v 是最后一个固定参数,初始化的结果是ap
指向第一个变参。
其他那两个本喵就不介绍了,遇到了自行查阅就好。
这一系列带v的输出函数,最后一个参数类型是va_list
,倒数第二个参数是format
,也就是最后一个固定参数。
这一族函数就是专门用来处理可变参数的,这里本喵使用的是vsnprintf
,将可变参数的内容全部放入到logcontent
中。
例如logMessage(NORMAL, "create socket success");
,这里面的字符串被当作了可变参数,因为只有一个字符串,所以将其传给了最后一个固定参数format
。
在我们的服务器中,在套接字创建成功后的日志信息中,增加_listensock
,以可变参数的形式传给logMessage
,如上图红色框中所示。
可以看到,成功打印出了监听套接字的文件描述符,如上图绿色小框中所示,在前面还有日志的前缀信息[日志等级][时间戳][pid]
。
日志不仅可以将数据打印到屏幕上,而且还可以输出到文件中,将屏蔽掉的代码解除注释。
将日志等级为NORMAL
和DEBUG
以及WARNING
放入到LOG_NORMAL
定义的文件路径中。
将日志等级为ERROR
和FATAL
的放入到LOG_ERROR
定义的文件路径中。
两个文件都是以追加写入的方式打开,写入完毕后再关闭文件。
在服务器中伪造出一些不同等级的日志信息,如上图所示。
服务器运行后,日志信息不仅打印在屏幕上,而且还输出到了日志文件log.error
和log.txt
中。
🧁守护进程
将服务器进程运行起来,如上图所示,再查看当前服务器上的网络进程以及进程。
可以看到,IP地址为0.0.0.0
,端口号为8080
,进程名为tcpserve
的进程是存在的。
- 直接关掉
Xshell
会话窗口,不退出进程。
此时再查看名为tcpserve
的进程,已经看不到了,说明它已经退出了,但是我们明明没有让它退出啊,只是关掉了Xshell
的窗口而已。
- 每一个
Xshell
窗口都会在服务器上创建一个会话,准确的说会运行一个名字为bash
的进程。- 每一个会话中最多只有一个前台任务,可以有多个后台任务(包括0个)。
当Xshell
的窗口关闭后,服务器上对应的会话就会结束,bash
进程就退出了,bash
维护的所有进程都会退出。所以关掉Xshell
窗口后tcpserve
进程就会退出。
这样就存在一个问题,提供网络服务的服务器难道运行了tcpserve
就不能干别的了吗?肯定不是。要想关掉Xshell
后tcpserve
不退出,只能让tcpserve
自成一个会话。
- 自成一个会话的进程就被叫做守护进程,也叫做精灵进程。
前后台进程组:
上图中,sleep 10000 | sleep 20000 | sleep 30000
是通过管道一起创建的3个进程,这三个进程组成一个进程组,也被叫做一个作业。后面又加了&
表示这个作业是后台进程。
使用指令jobs
可以查看当前机器上的作业,如上图所示,有3个作业在运行,而且都是后台进程。
- 前面的数组是进程组的编号,如上图所示的【1】【2】【3】。
通过指令gb+进程组编号
,可以将后台进程变成前台进程,如上图所示,此时Xshell
窗口就阻塞住了,在做延时,我们无法输入其他东西。
将该进程组暂停后,继续使用jobs
可以看到,进程组1后面的&
没有了,表示这是一个前台进程,只是暂停了而已。
使用指令bg+进程组编号
,可以将进程组设置为后台进程,如上图所示,此时进程组1后面的&
又有了,并且进程运行了起来,也不再阻塞了,可以在窗口中继续输入指令了。
可以看到,9个sleep
进程的pid值都不同,因为它们是独立的进程。
- PGID表示进程组的ID,其中PID和PGID值相同的进程是这个进程组的组长。
PGID
中本喵画了三个框,每个框中有3个相同的PGID
,所以此时就有3组进程,和前面使用管道创建的进程组结果一样。
但是所有进程的PPID
都是16496
,这个进程就是bash
,所以说,bash
就是当前会话中所有进程的父进程。
还有一个SID
,表示会话ID,所有进程的SID
都相同,因为它们同属于一个会话。
- PPID和SID之所以相同,是因为会话的本质就是
bash
。
🍦变成守护进程
要想让会话关闭以后进程还在运行,就需要让这个进程自成一个会话,也就是成为守护进程。
系统调用setsid
的作用就是将调用该函数的进程变成守护进程,也就是创建一个新的会话,这个会话中只有当前进程。
- 注意:调用系统调用
setsid
的进程在调用之前不能是进程组的组长,否则无法创建新的会话,也就无法成为守护进程。
如果调用成功,则返回新的会话id(SID
),调用失败,则返回-1,并且设置错误码。
如上图代码,daemonself
就是让我们的tcpserve
变成守护进程,总共分为四步。
- 让调用进程忽略异常信号
调用进程会变成守护进程,形成一个独立的会话,此时它的具体运行情况我们是无法得知的。但是它有可能会受到异常信号的干扰而导致进程退出,此时我们根本不知道它退出了。
所以要忽略掉异常信号,尤其是管道信号,因为网络服务程序就是在套接字中读写数据,所以使用signal
系统调用忽略掉SIGPIPE
信号。
- 让自己不是组长
setsid
系统调用要求调用的进程不能是进程组的组长,所以要想办法让我们的tcpserve
不是组长。
同样采用if(fork()>0) exit(0)
的策略,当前进程退出,将后续代码交给它的子进程,此时原本的组长就退出了,子进程成为孤儿进程被操作系统收养也就不是组长了。
子进程再调用setsid
变成守护进程,自成一个会话。
- 守护进程本质上就是一个孤儿进程。
- 关闭或者重定向以前进程默认打开的文件
在Linux中存在一个黑洞文件/dev/null
,向该文件中写入的内容会被全部丢弃,从该文件中读取内容时什么也读不到而且不会发生错误。
每个进程都会默认打开文件描述符为0,1,2
的三个文件,而守护进程是脱离终端的,没有显示器,没有键盘等,所以要对这三个文件做处理。
- 最好的方式就是将黑洞文件/dev/null重定向到这三个文件。
- 如果无法重定向,那就只能关闭这三个文件了。
- 进程执行路径发生更改(可选)
每一进程都有一个cwd
数据,用来记录当前进程的所属路径,所以默认情况下,进程文件所在的路径就是当前目录。
成为守护进程后,如果需要更改tcpserve
的执行路径,就可以通过系统调用chdir
来改变cwd
属性,从而更改路径。这里给的缺省值null
。
如上图所示,在tcpserve.cpp
中调用daemonself
,将tcpserve
服务进程变成守护进程。
在运行服务端程序后,服务器进程初始化,然后变成守护进程并且开始运行(这一点我们看不到)。当前会话并没有阻塞,仍然可以数据其他指令。
查看当前服务器上的进程时,可以看到守护进程tcpserve
的存在,并且它的PPID
是1(操作系统),PID
,PGID
以及SID
三者都是11251
。
- 守护进程自成会话,自成进程组,和终端设备无关。
此时本喵的服务器仍然可以干其他任务,tcpserve
守护进程也在运行,并且自行按照我们写的逻辑来接收客户端的连接请求并进行网络通信。
虽然有一个系统调用daemon
可以让一个进程变成守护进程,但是它并不太好用,实际应用中都通过setsid
自己实现daemon
的,就像我们上面写的一样。
🧁总结
现在TCP网络通信也见过了,虽然不知道原理,但是我们知道套接字的存在,以及网络通信的代码过程,和现象。除此之外本喵还介绍了常用小组件日志
,以及如何让一个进程变成守护进程。