网络基础(1)网络编程套接字TCP,守护进程化

TCP协议

下面我们来学习一下TCP套接字的使用。

也就是使用一下基本的接口。首先TCP套接字的使用和UDP套接字的使用是大同小异的,但是多了一些步骤。

这里回顾一下:UDP是不可靠的,无连接的协议。而TCP则是可靠的,面向连接的协议。也就是说客户端和服务端要进行通信,首先要建立连接。

简单服务端

下面先来写一个TCP的框架,依旧是Main.cc,Log.hpp,tcpserver.hpp,Makefile文件。暂时是这些。将Log.hPP移动过来之后。

首先来写Main.cc将主逻辑写好:

初始化服务器和服务器的开始函数之后再去填写。和上面写的UDP服务器是一样的。作为一个服务器首先服务器的端口号和sock是需要的,但是在TCP这里如果使用和UDP一样的sock是会出现问题的,什么问题,我们下面会说明。至于IP地址在服务器的类中是不需要的,首先服务器是不允许绑定一个固定的IP端的,并且服务器是要做到接收多个客户端的请求的,所以在Tcpserver这个类中是不需要IP地址的。

所以在TcpServer这个类中只需要端口号和sock(存在问题)成员。然后我们需要完成一个Init初始化函数,构造函数,和Start函数,为了让这个TcpServer类不能被拷贝我还是将之前写的nocopy.hpp拿了下来,让TcpServer继承这个类,让TcpServer不能被拷贝。

而要启动这个TCP服务器的指令也很明显是./TcpServer <端口号>,所以也是需要读取bash的启动字符串获取端口号的。而在这里就会存在可能失败的情况,所以也需要将comm.hpp从UDP那里获取一下。

TCP协议

下面我们来学习一下TCP套接字的使用。

也就是使用一下基本的接口。首先TCP套接字的使用和UDP套接字的使用是大同小异的,但是多了一些步骤。

这里回顾一下:UDP是不可靠的,无连接的协议。而TCP则是可靠的,面向连接的协议。也就是说客户端和服务端要进行通信,首先要建立连接。

简单服务端

下面先来写一个TCP的框架,依旧是Main.cc,Log.hpp,tcpserver.hpp,Makefile文件。暂时是这些。将Log.hPP移动过来之后。

首先来写Main.cc将主逻辑写好:

初始化服务器和服务器的开始函数之后再去填写。和上面写的UDP服务器是一样的。作为一个服务器首先服务器的端口号和sock是需要的,但是在TCP这里如果使用和UDP一样的sock是会出现问题的,什么问题,我们下面会说明。至于IP地址在服务器的类中是不需要的,首先服务器是不允许绑定一个固定的IP端的,并且服务器是要做到接收多个客户端的请求的,所以在Tcpserver这个类中是不需要IP地址的。

所以在TcpServer这个类中只需要端口号和sock(存在问题)成员。然后我们需要完成一个Init初始化函数,构造函数,和Start函数,为了让这个TcpServer类不能被拷贝我还是将之前写的nocopy.hpp拿了下来,让TcpServer继承这个类,让TcpServer不能被拷贝。

而要启动这个TCP服务器的指令也很明显是./TcpServer <端口号>,所以也是需要读取bash的启动字符串获取端口号的。而在这里就会存在可能失败的情况,所以也需要将comm.hpp从UDP那里获取一下。

支持无连接的,不可靠的,有最大长度的通信方案。

而这里需要选择的是:

提供全双工的,基于连接的字节流服务。至于最后一个参数也可以设置为TCP对应的那个参数,也可以写成0,因为前面两个参数已经足够让os去判断这里使用的是什么协议了。

至于返回值则是一样的,错误返回-1并且错误码被设置,成功则返回一个文件描述符。

下面是第一步创建套接字的代码:

既然返回的是一个文件描述符,说明创建的这个套接字就是一个文件,而文件要进行网络通信,下面就是要对文件填充一下本地的网络信息。并bind,再次说明这里的bind和c++中的bind在用法和作用上是完全不同的。

​bind​​ 接口通常用于将一个套接字(socket)与一个特定的地址(包括 IP 地址【一般不写】和端口号)绑定在一起,使得该套接字可以在网络中唯一标识出来,以便其他网络节点可以与其通信。

这里稍微再提一下,在一个os中联网的进程是有很多的,所以创建的套接字也是有很多的,所以os也是需要将这些套接字管理起来的,如何管理先描述在组织。对于这部分更加详细的信息在之后的章节再去说明,这里专注于TCP接口的使用上。

下面继续填充网络信息。

到这里初始化的第二步填充网络信息就完成了。

但是到这里并没有将这些信息设置到内核空间中(还是在函数栈,也就是用户空间中),所以下面就是2.1步bind。

因为bind函数需要将struct sockaddr_in结构变成sock_addr结构至于原因上面已经说明过了。这里可以将这个强转写成一个宏来完成强转。

到目前为止,TCP套接字的使用和UDP是没有太大的差别的。

而下一个步骤就是TCP协议特有的动作了。

之前我们说过了TCP协议是面向连接的协议,在通信之前就需要建立连接,那么谁来建立连接呢?一般都是客户端来建立连接的。而server端一般都应该主动等待连接的到来。正如开餐馆的老板都是被动的等待客户的到来的。这就要求餐馆的老板一直在餐馆中,不然用户来了没有人为其提供服务。所以TCP服务器还需要做第三步叫做:设置套接字为监听状态(TCP特有)

由此就要知道TCP特有的接口:

这个函数的第一个参数就是要设置的套接字的文件描述符,而这个函数的第二个参数暂时无法解释,之后会说明,这个参数是全连接队列。暂时不解释。这个函数成功的时候0被返回失败的时候,-1被返回错误码被设置。

下面就来进行这一步的代码编写:

下面运行一下:

到这里TCP服务端的初始化函数就完成了。

为了表示服务器当前的运行状态使用一个bool类型的成员变量表示当前服务器的运行状态。

构造函数那里使用false表示一开始这个服务器处于的是非运行状态。

那么要完成Start函数,首先就要将服务器的运行状态进行修改。

监听机制

对于TCP而言,服务器现在已经完成了基本的初始化函数sock也处于了监听状态了,现在也就是需要等待客户端来连接了。那么服务端怎么知道一个客户端连接了字节呢?所以还需要让服务端获取连接。如何获取需要使用新的接口:

下面使用示例图说明一下:其中一条线是客户端,其中一条线是服务端,然后服务端有一个套接字是处于listen状态的(下面的3表示的就是这个sockfd是3处于监听状态)

然后客户端一定会来连接这个服务器。所以服务器一定要从这个listen套接字中将这个连接拿上来。

那么是谁来连接这个服务器呢?是客户端那么服务器如何得到这个客户端的信息呢?就是通过accept套接字,对于accept套接字来说,后面两个参数是一个输入输出型的参数。更强调的是输出。这也就是说当服务端通过accept获得连接之后,这两个参数中储存的信息就是客户端的套接字信息。这两个参数等同于在UDP套接字中的recvfrom套接字中的最后两个参数是一样的(作用)。

回到这个accept套接字的参数上,第一个参数sock是是什么呢?这个参数就是上面我们使用socket创建的文件fd(完成bind,和listen的sockfd)。而这个套接字最重要的就是返回值。下面我们来看一下这个套接字的返回值是什么

上图中说的accepted socket就是在accept套接字中的第一个参数sockfd。可以看到这个accept套接字在成功之后又返回了一个新的文件描述符(新的sockfd)

那么这个新的sockfd又有什么作用呢(如何理解这个返回值呢?)

新的sockfd

在UDP代码的编写中从头到尾都只有一个sockfd。那么TCP为什么要创建一个新的sockfd呢?之前我们说过在tcp中服务端要等待连接的到来,而为了获取新连接就需要使用accept套接字,而这里新增了一个sockfd,那么未来服务端和客户端进行通信的时候要使用哪一个sockfd呢?

下面举一个例子

下面是一家饭店,然后饭店外有一名招呼客人的人叫做张三,饭店外有路人路过的时候,张三一直在拉这些路人到饭店中去吃饭。这个饭店的名字叫做好再来鱼庄。

有一次张三招呼到了几个人,然后张三会跟着这些客人来到饭店门口,往饭店中说一声来客人了。来一个服务员,提供服务。此时这些被招呼的人就去到了一个位置上,一会之后就会有一个服务员(李四)来为这些人提供服务。而张三又返回到外面去拉客人。

画图表示:

过了一会张三又带领了一些客人进来了,然后张三又会通知饭店让其出来一个新的服务员去提供服务。这个新的服务员王五又会为这一批客户提供服务。

在鱼庄中有客人的时候,张三这个人从来都不会进饭店,也不会为里面的人提供服务。

张三的核心工作就是从路上抓人来饭店。抓完一波,再去路边抓另外一波。

如果我是这个鱼庄的老板,此时我的员工是分成两个类别的。一类是李四王五,这种向外提供服务的服务员。还有一类像张三这样的服务员(也是饭店的客户来源)。而这里我们将李四王五叫做,accept新返回的文件描述符。而张三就是listensock,主要作用就是获取连接。而未来服务端和客户端要进行通信使用的文件描述符则是accept新返回的这个文件描述符。

所以下面我们要修改一下TcpServer中sock这个变量的名字了,这个不是sock了应该是listensock

然后改变一下上面使用_sock的代码。

下面就是来写Start函数了。

很明显就是要获取连接了accept接口,从哪里获取呢?从_listensock中获取。

既然张三是拉客的人,如果某一次张三拉客失败了,自然是不会放弃拉客的,而是马上去拉下一波客人。这个也要影响到我们的代码,在之前的代码中如果创建sock失败了就直接,失败不玩了,而accept这里即使失败了,也不会有任何的影响。

对于提供服务的代码一共有四个版本,这里先完成一个版本之后去写客户端,让两个终端能够互相通信起来。

为了不让代码变得臃肿这里专门写一个提供服务的函数。

下面首先就是要从客户端中读取信息了,在UDP那里可以使用recvfrom,因为UDP套接字不是面向字节流的,是面向数据报的。所以这个UDP和文件本身的渐进性不强。但是如果要进行TCP的读写(面向连接,面向字节流,和之前学习的管道特性是一样的)。所以对于TCP的读取直接调用read去进行读取,使用write进行写入。

下面就来使用read进行读取。

读取既然是一个系统调用就一定有可能会读取失败,如果读取到的数据等于0要怎么去处理呢?如果最后的返回值小于0代表读取失败了,直接break即可

对于read成功读取的时候,会返回读取到多少字节,而0表示读到了文件的结尾,也就是说如果read的返回值是0,代表读到了文件结尾。在网络当中,如果reada返回了0,表示读到了文件结尾(表明对端关闭了连接),这个特点和当时的管道是一模一样的,管道的四种情况。

下面是代码:

读取完成之后再将信息写回去,这里对于写入的返回值(写入是否成功暂时不处理)。

后面会说明写入的返回值。

这里为了测试就一直让其进行IO的操作。在上面的代码中有一个小细节,在写的时候使用的是sockfd,而这里再读的时候使用的也是sockfd。这也就说明了,这个新的sockfd(tcp连接)是一个全双工的。

服务器暂时这么写。以上就是服务端的书写了,然后来运行一下。

在启动了服务端之后,使用下面的指令去查看信息:

netstat -nltp
其中的n代表将能够显示成数字的字段全部使用10进制数字显示
p代表显示进程相关的信息
t代表查看tcp
l代表只查看listen状态的TCP套接字

这就说明我们的服务器已经处于监听状态了,而这里为了完成测试下面就来编写客户端。

简单客户端

然后就是和UDP一样的操作了获取服务端的ip和端口号然后去创建套接字。

下一个步骤就是bind了,那么客户端一定要有自己的ip和端口,那么要不要bind呢?答案是不需要显示的bind,但是是需要bind的,因为客户端也是需要有自己的ip和端口的。如果bind了一个固定的端口,就可能出现问题(其它的线程占据了这个端口就会导致当前的客户端无法启动),也就是说一般的客户端都是随机端口的

而在TCP中在client发起连接的时候客户端会被os自动绑定本地端口号。

在UDP那里直接就可以让client去发和收取信息了。但是TCP是面向连接的协议。所以客户端的下一步就是建立连接。使用的接口:

这三个参数也就不详细解释了,第一个参数是client创建的sock,而后面的两个参数都是说明要连接的服务端的信息的。

这里会涉及到将字符串风格的地址转化为4字节整型的地址,使用的接口是inet_pton。这个接口能够完成两件事情:

第一个将点分十进制的ip转化为4字节的整型ip,第二个:将四字节ip转为了网络序列。

并且会将这个信息写到struct sockddr_in的结构体变量中去。

但是到这里了,这些信息也没有被设置到内核中去,因为这些结构体本质还是在栈中的,属于用户空间下面就是调用connect接口将这些信息写到内核中去了。

下面就是代码:

如果连接失败(connect)返回的是-1(一般情况连接失败:要么是网络的问题,要么就是服务端没有启动)。如果是网络问题,那么就要给客户端一个重新连接的机会这些功能的实现都会放到后面。这里默认连接失败就直接return。

connet之后客户端就会由os自动的去选择一个端口。也就是说未来进行通行的时候客户端和服务端各自都有自己的ip和port。

客户端这里没有产生新的sockfd,连接成功之后使用之前创建的sockfd就能完成和服务器的通信。

下面我们就让客户端去给服务端发送信息。同时客户端也要接收服务端发送的信息

这里发送信息失败以及收取信息失败的处理方式这里都没有做更加具体的处理。

下面就是测试了。

同时从这一个日志信息上可以看到在服务端accept之后确实形成了一个新的sockfd为4,那么为什么是4呢?因为这里是单进程。现在只有一个客户端连接过来所以是4,再有客户端来连就是5。

而如果这里将客户端关闭之后,会发生什么呢?

可以看到服务端也是能够知道的(原因就是在客户端关闭之后,客户端对应的套接字信息也就没有了,服务端就会读取到0,也就知道了客户端关闭了)。

而在服务端知道客户端关闭之后就会从server函数中退出,让4文件描述符不再使用,然后再次去等待,连接。所以当存在一个客户端再次连接到服务端之后,因为之前的4已经被释放了,所以再次获取会再次获取到4.

现在我们启动服务器和客户端使用指令去查看一些信息。

可以看到客户端到服务器以及从服务器到客户端互相建立了两条连接。

怎么会出现两个连接呢?这里的原因是我的客户端和服务器是在同一台主机上进行的。如果是在不同的主机上就只有一条连接。

现在我在启动一个客户端

可以看到我新启动的这个客户端怎么无法发送信息到服务端呢?另外有一个客户端又是可以发送的。

然后我将下面的这个客户端关闭。

服务器一瞬间就收到了这些信息,并且将这些信息发送回去了,造成这个的原因就是这个服务器代码是一个单进程的代码,一旦进入Server就进入死循环了。也就是当前的这个服务只能同时处理一个链接。原因也很简单,当服务端接收了一个链接之后,执行流就进入到了Server这个死循环中,自然就不能接收其它的客户端的链接了。

对于这个问题,暂时先放在这里,我们先去完成一个断线重连的服务。也就是让客户端能够支持断线重连。

断线重连

首先是定制重连的次数:也就是定义一个无法改变的常量/宏。很多游戏的客户端(QQ等)都会做这个操作的。

然后我们封装一下下面的这些操作。

我们之前在客户端所做的创建套接字,填充套接字,隐式bind,然后和服务端建立connect都是在访问服务器。我们就将这些操作进行一些封装。

既然封装了访问服务器的操作,那么如果创建sock失败自然就是访问服务器失败了,connect失败了也是一样的,最后如果你向服务器写入失败了,自然也可以认为是访问服务器失败了,直接返回。然后是读取服务器发送过来的信息(read的返回值),如果read的返回值 == 0表示服务器被关闭了,或者协议是这么规定的。直接break循环即可。但是如果读取失败了,也就是访问服务器失败了直接返回即可。然后就是代码了:

但是这样写有一些不太好,机会每一次异常的返回都要先关闭close(sock)然后才能去返回。这样就完成了队服务器访问操作的封装。

那么现在重连的思路也很明显了,每一次的重连都是在调用上面写的这个函数而已。

下面就来写代码:

如果重连次数大于限定次数说明服务器是离线的状态。

现在来测试一下:

这里我让服务器连接一个没有运行的客户端,确实是能够重连的。但是这里存在两个问题。

第一个:如果在重连的过程中有一次重连成功了,然后再次连接失败了,那么再次重连的时候又会重新拥有REY_TRY次的机会。所以visitserver这个函数必须让客户端在重连成功后,让count重新归为1。为了完成这一步骤,就需要将count的地址传递过去,同时在这个函数中的conect成功之后就直接让count设置为1。

然后我们继续去运行就会出现下一个问题:

这里我先让客户端连接成功之后,直接关闭了客户端。

然后在输入信息之后可以看到并没有触发重连而是整个进程都被杀死了。

原因就是当使用当我们将信息写到客户端中的缓冲区之后,这个缓冲区会将这个信息通过write写到服务器中,但是服务器关闭了,此时就会出现异常,然后os注意到这个异常直接就将客户端进程杀死了。

但是如果我没有输入任何的信息是可以进行重连的。

可以看到确实是触发了重连。

但是这也不能看到重连之后的count被重置啊,要做到这一点还有一个点需要处理,如果直接去做实验会导致下面的现象:

可以看到我左边在重连服务器的时候出现了一个Address already in use的错误。"Address already in use"错误通常是由于在服务器关闭后,之前使用的端口还处于TIME_WAIT状态,而客户端尝试重新连接时遇到了这个问题。

对于这个错误更加详细的原因我在后面的网络原理会说明。这里暂时只说明如何处理

使用下面的接口:

对于这个函数的解释也放在后面说明

在服务端的Init函数中写:

再去尝试一下原先的实验,看在重连之后count是否会被重新设置为1:

看到确实被设置了。

对于上面setsockopt函数后面讲解TCP原理的时候再去详细的说明。

但是到目前为止,服务端都是一个单进程版本的服务器,也只能支持处理一个客户端,下面就要将其变成一个多进程版本的服务器。

多进程版本服务器

要将我们的服务器改成多进程版本的,就要考虑下面的问题:

我们的每一个进程都会有自己的文件描述符表,这个表中012默认占据的是标准输入,标准输出,和标准错误。

然后我们每打开一个网络套接字在底层其实也是一个struct file对象。

我们的这个单进程的代码对于连接的第一个客户端,占据的文件描述符表中的第三位,此时再来一个客户端连接,就是第四个位置。

那么现在如果建立一个子进程。对于子进程来说哪些东西要被重新创建呢?首先就是进程的pcb肯定是要重新拷贝的,然后文件描述符表也要重新拷贝。

那么这些往后的(struct file对象等)用不用重新创建呢?肯定是不需要了。

子进程拷贝出来之后文件描述符中3号位置最后还是会指向和父进程一样的file对象,同样四号也是一样的。

由此我们就可以让父进程去获取新连接,而让子进程去处理这个新连接。

此时就需要让父子关闭不需要的文件描述符了

关掉之后就能让子进程去处理新的连接,而让父进程去获取新的连接。如果不关带来的结果就是:父进程获取的连接数一旦达到了文件描述符表的上限就无法再去获取新的连接了(这种情况也就是文件描述符泄漏)。对于子进程没有太大的影响。为了处理这种情况--父子进程推荐关闭自己不需要的fd。

下面就是写代码了:

这个代码是在tcp_server类中的Start函数中的。但是上面的代码还是存在问题的,这个问题就是如果是让子进程去进行server,因为父进程必须要等待子进程,此时就变得和单进程的服务器没有什么区别了,只不过是父子去串行处理的。那么能否让父进程在等待的时候不选择阻塞等待呢?也不行,想象一种场景,现在有100个连接过来了,然后父进程处理了这些连接,然后就再也没有连接过来了,此时就会出现父进程一直在等待获取连接的接口处等待。不会去执行waitpid函数,导致僵尸进程的出现。

对于这个问题有两种解决方法:

第一种:子进程在退出的时候会向父进程发送SIGCHLD 信号,也可以通过基于信号的方式来回收子进程,这种方式是可行的。

但是不够简单和优雅。

这里使用第二种方法:

让子进程继续去创建子进程,也就是创建一个孙子进程。在创建完成之后直接让子进程退出。因为子进程只执行了很少的代码,所以父进程也能很快的等待到子进程。也达成了让父进程去快速的处理其它的连接。回到子进程和孙子进程上,当子进程退出的时候孙子进程就变成了孤儿进程会被os接收。所以也不需要担心孙子进程的资源泄漏问题。

是否可行呢?测试一下:

同时去看一下进程的个数

可以看到确实是存在了三个进程,并且有两个进程的父进程是1,而1自然就是bash(可以认为就是os了)。我们去看server端的信息,可以看到每一个连接过来的客户端最后得到的sockfd都是4,为什么都是4呢?原因就是父进程每获取一个连接,就会创建子进程然后让子进程继承下去,然后父进程就会关闭这个文件描述符,所以这里获得的都是4。

保证了不会出现文件描述符泄漏。并且我们写的这个代码也是具有重连的功能的。

那么除了这种方案之外还有没有其它的方案呢?

多进程信号版本

答案是也有。下一个版本就是多进程的信号版本。

前面依旧是一样的去创建多进程,不一样的是现在父进程不想去等待子进程了。此时就让子进程直接去提供服务,然后让父进程不进行等待,直接去获取新的连接。此时没有进程去等待子进程就会出现系统资源越来越少的现象,为了处理这样的现象。

就需要处理子进程向父进程发送的信号了(每一次子进程退出的时候都会向父进程发送SIGCHLD信号)。其中的一种处理方法就是捕捉这个信号然后重写一个信号的处理方法,而这个信号的处理方法就是去wait子进程,但是在Linux中也可以选择直接忽略这个信号。

因为在Linux中如果对SIGCHLD信号进行忽略,子进程在退出的时候,就会自动的释放自己的资源。

下面是代码:

下面再来运行一下:

然后再退出几个客户端:

通过脚本可以看到没有出现僵尸进程的问题。如果没有使用忽略这个信号此时这里就会出现僵尸进程的问题。

以上就是多进程基于信号版本。

但是这个代码仍然是存在问题的,因为创建进程也是存在代价的,上面的代码都是在客户端连接到服务器之后才创建的进程。此时就将代价转到了客户端上(客户端需要等待进程的创建),那么能否引入进程池呢?当然可以。进程池的代码之前也是写过的。

这里的思路也很简单,将server包装成进程池中的进程能够执行的代码。每连接到一个客户但了就负 载均衡式的唤醒一个进程去执行server代码即可。但是这里就有一个问题。在进程池中的进程是提前创建的,而上面的代码都是来了链接之后才去创建的子进程也就是先有的链接,再创建的子进程。而在进程池中因为子进程是提前创建的,也就导致了子进程不能获得这个链接,所以我们就需要处理一下,让之前创建的子进程也能得到这个链接。

这里就需要使用一种技术了,也就是使用Unix域套接字。

因为笔者写这篇文章的时候没有写这个代码,所以就不写代码了。但是思路是这样的。

下一个版本:多线程

多线程版本服务器

现在解释原理,当父进程创建一个一个子线程之后,是会和主线程共用同一张文件描述符表的。所以主线程曾经打开的文件描述符,也能让子线程直接知道。这也就意味着在多线程中根本不需要进行父子间文件描述符的传递。所以多线程的服务比较简单。

除此之外主线程和新线程不需要关闭不需要的文件描述符。因为主线程和新线程共享一张文件描述符表。

这里使用原生的线程,不使用我之前封装的简单的线程库了。

但是使用线程的话需要考虑主线程需要join子线程,就会让主线程无法一直获取链接。所以当创建好一个子线程之后需要将这个子线程设置为分离状态。

那么现在要解决的问题就是要让Thread_Start函数拿到sockfd了。这里不能使用pthread_create(&id,NULL,Thread_Start,&sock)的方法,因为这里你不能确定主线程和子线程是谁先运行的,如果某一次主线程运行了两次,那么某一个子线程得到的sockfd就是错误的(原先正确的sockfd被覆盖了)。所以这里需要解决这个问题。

虽然完成了但是可以看到还是存在报错的,这个原因就是Thread_Start函数是在类内部的。所以其实这个函数是包含了一个this指针的隐藏参数的。

所以需要将Thread_Start方法设置为静态的。

但是这样做之后,又会导致编译器不知道Server方法是哪一个类对象在调用了。所以我们需要在ThreadData中在将一个this成员变量。

测试一下是否可以运行。

运行结果:

此时如果再次增加一个客户端就会发现,文件描述符一直在增加,原因很简单,当前只有一个进程,而多个线程是共享一张文件描述符表的。

但是这里还是有一个问题,客户端来了才创建多线程是存在代价的,所以这里的方法就是引入线程池,让线程提前创建好,并且因为多个线程公用同一张文件描 述符表所以不需要担心提前创建的线程得不到文件描述符的问题。

对于这个问题之后再去解决,现在的问题是服务端得到了客户端的信息,但是服务端并不知道客户端是谁啊。所以需要增加一些代码。

对于服务端而言有一个函数就会将客户端的信息返回过来。

accept函数,在成功的时候会返回客户端的信息。

而在之前我们已经写了一个InetAddr类能够完成将sockaddr*由网络序列转化为主机序列了。

然后让ThreadData中新增一个成员,也就是这个InetAddr类。

然后重写一下Server函数。

运行结果:

这样服务端就能得到客户端的信息了。

对于实现这个功能其实并不难。我们需要关注的是,将网络序列转化为主机序列。在InetAddr这个类中我们使用的接口是下面这两个:

inet_ntoa能够将4字节的网络序列转化为主机端的点分十进制的地址。当时我写这个接口也是因为这个接口便于理解。

可以看到这个接口返回的并不是这个字符串,而是这个字符串的地址。那么这个接口转换完成后的字符串在哪里呢?

这个问题真如之前学习的c接口fopen函数,这个接口最后返回的是一个FILE*的变量,那么FILE在哪里呢?这个FILE是fopen在内部malloc出的一个空间然后将值拷贝过去的。

而这个inet_ntoa采用了类似的方法,而这里是多线程的环境啊,这就可能会造成线程不安全的现象。

这个函数使用的是静态的缓冲区。

例如下面的测试:

两个地址在转化完成之后变成同一个地址了,说明这个缓冲区永远只保留一次的结果。

在APUE中明确指出了inet_ntoa是线程不安全的。但是我在centeros7中测试了没有出现这种问题,可能是centeros7内部使用了互斥锁。但是既然可能存在线程安全的问题,就需要将这个函数替换了。

之前我们使用过一个函数inet_pton,这个函数能够将一个字符串ip转化为4字节ip的网络序列。而这里依旧存在一个函数能够将一个4字节IP的网络序列转为位主机端点分十进制的IP地址。

第一个参数位协议家族,第二个参数就是四字节的IP地址(网络序列),第三个就是转化完成之后储存的位置。最后自然就是大小了。这个函数的返回值,成功返回的其实就是dst的地址,失败一般返回的都是NULL,也有将-1强转的。

这就意味着每一个想要调用这个函数的进程都必须自己维护一个缓冲区,和大小。此时就能有效的避免线程安全的问题。

然后我们就来修改InetAddr类中的代码:

线程池版本服务器

现在回归主题,让线程池加入到这个代码中。

依旧是先让线程池需要的组件拿过来。

因为我实现的这个线程池也使用了ThreadDate类,为了防止命名冲突的问题。我这里就使用命名空间了。

然后就是要将这个线程池引入到tcp服务器中了。

服务器在初始化的时候就可以将单例线程池创建完成了。也就是要将获取单例的函数放到服务器的初始化函数中:

然后尝试启动一下线程池。因为我将线程池中储存的类型设置为了int而int类型是没有()方法的,所以这里我就将线程池中的()方法注释了。

现在已经有了线程池之后在服务端的就变成了,服务端获取到了链接之后,将链接push到线程池中,线程池就会唤醒线程去处理这个任务。

那么此时自然也就不需要创建线程的步骤了,既然不需要创建线程的步骤,自然能ThreadData也就不需要了。

现在我们要完成的事情就是将线程池中线程要执行的任务传递过去,线程池就会唤醒线程去执行。

这里我就设定一个类型然后将这个类型填入到线程池中。

然后当服务端收到一个链接之后就构建一个task_t传递给线程池即可,这里可以使用c++中的bind函数,将Server函数绑定给task_t类型的函数。

需要注意一下我之前重载过Server任务,这里就会出现不知道绑定的哪一个版本的Server的问题,所以写到这里的时候需要将之前重载的Server给注释了。

然后测试运行一下:

这样就完成了。

但是这个代码还是存在问题的,是可能存在客户端无法链接的问题的。原因也很简单,这里是一个线程池,而线程数量是固定的。而这里提供的服务是一个死循环的服务,这种服务一旦一个线程进入了,就不会退出,这个线程就只能为一个客户端提供服务了。就会导致有的客户端无法获得服务。

而多线程一般很少让任务成为一个长任务长任务的处理(在多路转接会说明)。

也就是目前我们不能让我们的服务器提供长服务。

现在写的这个服务器只能提供基本的IO服务(服务器接收发送消息)。因为协议我还没学习,所以不能增加太复杂的服务

下面我们提供几个服务,要如何处理呢?

为了提供不同的服务我们需要一个哈希表。

然后再提供一个函数类型,这个函数是返回值为void,参数为int(sockfd)和InetAddr类型(客户端的信息)。

然后就是构建业务逻辑了。

这里的业务逻辑就是通过字符串选择不同的callback_t函数。

然后需要增加一个函数用于给func函数增加方法。

然后需要提供一个路由的方法,所谓的路由也就是判断用户需要的是什么服务,既然要了解用户需要的方法,自然要读取用户写的信息了。读取之后拿这个信息和哈希表中的key做比较然后选择正确的服务。

这样就完成了一个功能路由的方法。

现在已经有了一个功能路由的方法,当服务器接收到了客户端之后,线程为客户端现在不提供server服务,而是提供一个功能路由的服务。

当某一个客户端链接到服务器之后,经过路由就能够选择不同的服务了。

然后我们就需要去到服务器的main函数中去给这个服务器注册服务了。

在服务器初始化完成之前加载对应的服务。

需要将服务列表给客户端打印一下。

现在想要使用什么服务就由客户端自己去决定了。

先来修改一下客户端:

客户端需要让用户去选择自己需要的服务。

在选择了服务之后,下面的输入就是针对各个服务的输入了。

我们现在完成最简单的ping服务。

现在先测试一下所以这个ping服务我们就将之前的server代码拷贝过去。

测试结果:

成功了。然后就是其它的服务了。

现在这个服务器要提供什么服务,就由我们自己去编写了。

然后在路由的函数出也增加一个日志信息显示某一个线程选择了什么服务。

然后在每一个服务函数中也打印一下这个信息。

然后再去测试一下:

可以看到这个打印信息。说明现在已经能够跳转到各个服务函数了。

线程池ping服务编写

下面我们来实现第一个服务ping服务,这个ping服务其实就是一个IO服务。只不过当客户端选择了这个服务之后,服务端线程只会运行一次这个服务就会退出,不再是一个死循环式的服务了。

然后就是无论你客户端给服务端输入什么,服务端最后返回的都是一个pong。

然后对于路由函数也有需要修改的东西。当某一个线程执行完某一个客户端的任务之后也就代表着这个sockfd不再使用了,那么就可以关闭这个sockfd了。

所以在Routinue函数最后需要关闭sockfd。

运行截图:

可以看到当客户端选择了ping服务之后,除了第1次输入,服务器返回了一个pong,第二次输入之后客户端直接崩溃了。崩溃的原因也很简单,这里的ping服务只是一个短服务,也就意味着当服务器返回了pong之后,就直接将sockfd关闭了。当客户端再次输入信息的时候因为在向一个关闭的sockfd中发送信息就直接被os杀死了。

那么这个ping有什么用呢?这里需要知道的是未来的服务都是部署在云服务器上的,那么我如何知道我的服务在未来的某一个时刻是否是健康的呢?

那么这里我们就可以定期(30s)向我的服务器发送最小服务请求,如果得到了恢复,说明我们的服务是正常的。这种机制我们称之为心跳机制。

而这里的ping函数也就是对心跳机制的响应。

但是这样写的ping服务有一点点的重复。这里我们可以封装一个interact函数(交互函数),这个函数的功能就是将一个信息通过sockfd发送到某一个客户端。这里我们就可以认为是完成了一次交互。

然后我们来写下一个服务

线程池translate服务编写

首先明确这个服务的功能是什么这个服务的功能也就是你输入一个英语单词,而服务器将这个单词的汉语返回过来。

对于translate服务,有一种极其简单的方法,所谓的单词翻译无非就是你输入字符串,然后服务器直接返回一个新的字符串,那么可以选择使用一个unordered_map将将某一个单词作为key而汉语意思作为value。放到map中,然后通过客户端发送的英语返回value即可。但是这种方法储存的单词数量很少,map只能储存在内存中。而这里就选择了另外一种方法,将单词翻译作为一个简单的业务去写。也就是将这个业务和网络代码做一个解耦。

然后在这个构造函数中可以选择直接插入一些单词的信息到dict中,但是这里我们不这么做了。这样写就相当于将这个dict硬编码了,并且这样储存的单词数量也是很少的。

这里我们可以选择通过读取文件的方式来进行。

首先准备一个文本文档作为dict中信息的来源,然后运行的时候将这个文档的信息读取到_dict中即可。

这里我就准备了一个简单的单词表:

然后这里我们可以看到一个单词是具有单词本体,单词的音标,单词的词性。而单词的翻译也就是找到对应的单词本体然后返回单词的中文。也就是说这就是对单词的管理,既然是管理那么就可以使用先描述再组织。将单词写成一个类。然后在映射的时候使用单词和word之间进行映射。

这里为了简便一些我就不这么写了。

这里就直接客户端输入一个单词我这边把含义全部给你。

下面我们就需要去写一下加载这个txt文档的函数了。

对于读取到的信息,因为这里我的txt文档是一行一行的,所以先使用一个vector<string>储存起来。然后再对这些文档做字符串分析。

下面来测试一下读取是否成功了

注意这里我将.hpp修改为了.cc,并且将#pragma once去掉了,再测试完毕之后我会将其修改回来的。

测试结果证明读取文件成功了。

现在已经能够做到将文件读取出来了,但是我们需要的是翻译啊。

为了能够进行正确的返回这里将文件中的单词按照第一个空格作为分隔符,分成左半部分和右半部分。左半部分作为键值,右半部分作为value值。也就是对读取到的信息进行字符串分析。

当然这里也是可能存在问题的,例如如果某一个单词的空格是出现在了单词和英标的后面,或者再单词内部出现了空格的情况,这里我就不处理了,默认的就是符合规则的。当然你要处理也很好处理,无非就是读取字符串,然后对读取的字符串进行判断和修改的工作,例如如果是单词内部出现了空格你就使用一个指针如果某一个英文字符的后面出现了空格,但是这个空格后面又出现了一个英文字符则直接删除这个空格,多个空格也是一样的处理,这些情况都是需要进行处理的,但是这里我就直接默认没有错误的情况了。对于这些情况的处理可以写一个预处理函数,对数组中的每一个字符串先进行一次预处理。

这里我没有写,而是直接写了这个字符串处理函数。

这样每一个单词的意思就按照英文本体和中文被放到词典中了。

下面再测试一下词典中是否存在这些内容了,将这个函数放到构造函数中,然后调用debug打印一下

运行截图:

 

那么现在就能够使用这个类去向外提供服务了。

之后你想要加词的话,直接向这个文件中加就可以了。

我们还可以添加一个热加载的功能,这个热加载功能就是当这个类收到某个信号之后,回去进行加载文件和文件分析的工作,甚至于可以直接删除这个对象,重新创建一个对象,都是可行的方法。而这也是一种基于信号的热加载的方法。

下面我们将日志添加到这个类中,然后这个类就可以向外部提供服务了。

然后我们就可以去完成服务端的翻译函数了。

这样翻译的服务就完成了。

下面测试一下:

翻译服务就完成了。

写这个服务就是为了表示,现在业务已经能够拆出来了,那么就可以写一个聊天室服务,五子棋服务等都是可以写的。业务和网络代码的分离也就可以做到这一点,同时子复杂一点,我们现在写的ping服务也可以是一个登录服务,translate是一个注册服务等等。这些服务都是可以通过注册的方式写到这个服务器上的。

回到这个代码上,到这里还有一个小问题,再客户端连接的时候,服务端就向客户端发送信息表示能够提供的服务是什么。所以我们需要让服务端给每一个连接上的客户端发送服务列表。在server端的start函数中

然后修改一下客户端让其能够接收到这个信息,再去选择服务。

那么现在就能够使用这个类去向外提供服务了。

之后你想要加词的话,直接向这个文件中加就可以了。

我们还可以添加一个热加载的功能,这个热加载功能就是当这个类收到某个信号之后,回去进行加载文件和文件分析的工作,甚至于可以直接删除这个对象,重新创建一个对象,都是可行的方法。而这也是一种基于信号的热加载的方法。

下面我们将日志添加到这个类中,然后这个类就可以向外部提供服务了。

然后我们就可以去完成服务端的翻译函数了。

这样翻译的服务就完成了。

下面测试一下:

翻译服务就完成了。

写这个服务就是为了表示,现在业务已经能够拆出来了,那么就可以写一个聊天室服务,五子棋服务等都是可以写的。业务和网络代码的分离也就可以做到这一点,同时子复杂一点,我们现在写的ping服务也可以是一个登录服务,translate是一个注册服务等等。这些服务都是可以通过注册的方式写到这个服务器上的。

回到这个代码上,到这里还有一个小问题,再客户端连接的时候,服务端就向客户端发送信息表示能够提供的服务是什么。所以我们需要让服务端给每一个连接上的客户端发送服务列表。在server端的start函数中

然后修改一下客户端让其能够接收到这个信息,再去选择服务。

线程池中的transform服务

这个服务也就是将你输入的字符串中的所有小写英文字符修改为大小字符。

这个服务就很简单了,我也就不和translate服务一样去写了。

最后是测试的截图:

线程池中的default服务

还有一个default服务,这个default服务我打算写成一个打印当前服务器中提供什么服务的函数。也就是将上面我们写的那个打印服务器列表的函数封装到这个服务中。也因此这个函数也就不打算暴露给外部了。而是将这个服务的实现放到TcpServer类中去。

然后这里我将之前在初始化函数那里的信息传递删除了。而是将这个信息传递写到了,路由函数中。当线程执行到到这里的时候会先向客户端发送服务列表的信息。

然后在服务器的初始化函数这里就将这个函数直接插入到funcs中去

然后这个服务器现在提供的是一次性的短服务,所以客户端也不应该一直在任务窗口而是在完成一次任务后就直接结束。

如果想要修改为客户端一直不退出的话,就需要让服务端的服务线程在路由函数一直不退出,这样就能让服务器一直为客户端提供这些服务。直到某一次线程收到客户端的退出信息再关闭sockfd再让线程退出。但是这里我就不修改了。

将客户端任务窗口的死循环删除,再将错误信息修改后:

主要就是将死循环删除。

运行测试:

测试完成之后我们的这个简单项目就已经完成了。

小节总结

通过上面的代码我们已经知道了客户端和服务端能够使用read和write从网络中获取信息。由此我们就能得到第一个信息:

IO类的函数,write/read在底层已经做了转网络序列的工作

下一个信息和UDP和TCP协议的特点有关。

首先UDP是用于数据报的,而TCP则是面向字节流的。那么这个用于数据报和面向字节流有什么不同呢?

到目前为止,这两个东西在编码上的区别是很小的,但是从底层上来说这两个协议的实现是具有很大的不同的。

这里我们在编码上的区别很小是因为TCP代码中,我们在编写IO代码的时候,尤其是网络IO的时候,使用过的read/write,目前的代码都是存在bug的。在服务端读取客户端的信息的时候,服务端向客户端发送信息的时候,都是存在bug的。在UDP中这一份代码是没有问题的,但是在TCP代码中是存在问题的。要解决这个问题,需要使用协议。但是目前无法解决。

现在回到UDP和TCP的特点上:UDP是面向数据报的也就意味着:在UDP中数据和数据之间是存在边界的。什么意思呢?在UDP协议中使用的接口就是:sendto和recvfrom接口,

这就意味着当我们在客户端sendto了一次信息之后,(不考虑数据丢失的情况),那么服务端一定会进行一个recvfrom,反过来也是一样的。

拿现实的例子说明,当一个人给你发送了5个包裹的时候,你一定会接收到5个包裹。并且包裹和包裹之间区别是非常明确的。回到UDP也就是报文之间是相互独立的。

而现在使用的TCP则是面向字节流的。也就是说在TCP这里write了不止一次(10次),而在read那里可以使用一次直接接收完成,也可以使用多次接收完成。这个接收和发方式没有联系的。

这个东西也就是面向字节流。

拿之前学习过的管道说明,写端可以往管道中写无数次的信息,而读端可能只使用一次读取就将这些信息全部读取上来了,而这也就是面向字节流。

这就有可能造成,有一方每写一次都写了一个完整的报文,但是另外一方就是不读,而另外一方就是不读。而是一直积压在管道中。而在写了10次之后,再一次性全部读取。这也就意味着某一方不能一次性处理所有的信息,而是需要一个报文一个报文的处理。由此就导致了数据的解析工作要有用户层来做。这也就是面向字节流。

使用现实举例子,UDP就相当于发邮件寄信。然后你收信的时候,无论你是一次性收取所有的信还是一封一封的拿取。每封信之间的区别是很明显的。

在UDP当中也就是用户将报文拿取上来之后,是不需要判断这个报文和下一个报文之间是否是存在关系的。每一个报文之间都是单独存在的。这是UDP。

但是TCP不一样。TCP是面向字节流的。

就拿接自来水为例子,在家中使用自来水的时候,我是不关心这个自来水来到我家的时候,是自来水厂压送了几次的。我只关心我打开水龙头之后有水,然后我使用杯子接水还是使用桶接水都是由自己决定的。

而我现在使用的水很可能就是自来水厂压送了10次才送上来的。也可能我接了1000次,10000次水而自来水厂只压送了1次就送上来了。

此时发送方和接收方是没有明确的联系的。

自来水在管道中都是水流,而到了我家之后要怎么使用都由我自己决定。

假设接了一杯水就相当于将一个报文读取上来了,之后还有多少个报文我是不知道的,我必须边接边处理。这种特点也就是面向字节流。

正如在进行文件操作的时候,往文件中写是最容易的。 但是从文件中读取的时候是恶心的

因为可能在写的时候,我是按照一个单词一个单词的写的,所以在读取的时候,我也希望一个一个读取单词。但是在写的时候我一行写了10多个单词(使用空格隔开)。由此就导致了我在读取的时候,要么直接将整个文件读取上来然后去寻找空格,要么就是一个字符一个字符的读取。直到遇到空格。由此文件本身也叫做字节流。这个也就是TCP的特点

到目前为止,上面写的代码都没有对字节流进行处理。就拿大小写转换的服务为例子,在这个例子中我们定义了一个1024字节的缓冲区大小,但是我怎么就确定用户输入的信息一定是1024字节大小的呢?如果用户传入了一篇文章这个文章是10000字节呢?

这样我是无法证明服务端将客户端的信息读取完成的。

而在TCP中要正确的处理信息的读取,必须要结合用户协议。

也就是要完成自定义的协议之后才能正确的读取。

就拿刚才的以空格为分隔符输入单词为例子,当在读取的时候以空格为分隔一个一个读取单词。这就是在文件和进程之间建立了一个协议。这个协议就是读取信息的一方如何判断一个单词呢?就是通过当读取到一个空格的时候就能够认为前面读取的信息就是一个单词了。

由此也就能够知道了TCP的代码是更难写的。

这里先暂时理解到这里。之后再去说明。

现在回到我们的代码上,难道当我们启动了服务器之后,我们的服务器就要一直以前台进程的方式在前台运行吗?当然不是这种模式最多在Debug的时候使用,真正的服务器(我们刚刚写的那种软件),必须在Linux后台,以守护进程(精灵进程)的方式进行运行。

那么什么是守护进程呢?

守护进程

首先守护进程并不算是网络的概念,这是一个系统的概念。

为了理解这个守护进程需要知道在进程中其实不仅存在进程的概念,还有进程组,作业,会话这样的概念。

下面我们来复习一下之前所说的前台和后台的概念:

这里我们先启动两个bash,然后在一个bash中使用sleep10000。

然后我们在另外一个bash中就能查询到我们刚刚启动的进程

这个进程的id是576 ppid为547。

当我们重新启动了一个sleep之后,pid变了,但是ppid依旧是不变的

下面我们来认识一下其它的id:

上图中的PGID就是进程组ID,SID也就是会话ID

而TPGID一般都是和PGID一样的。TTY是当前打开的终端设备是谁

stat表示当前进程的状态,而UID表示当前用户的身份,我这个用户在系统中的编号就是1000.

因为现在我只启动了一个sleep 10000的进程,所以这个进程的PID就等于它的组ID,所以当前这个进程就是自成进程组。但是无论怎么样,这个进程组一定是属于当前的会话的。(也即是这个进程组是在这个会话当中的) 。

下面我们再启动一批进程,然后再去看一下

通过管道我们知道这三个线程都是兄弟线程。

此时这三个进程的父进程都是同一个所以这三个进程是兄弟关系,所以能够使用匿名管道通信。然后我们还能看到这三个进程的PGID也是一样的。此时这三个进程就是一个进程组。

而这个组ID一般是多个进程中第一个启动的进程的PID。

然后PPID也就是bash的id。

然后我们就能够知道了:

当我们登录Linux的时候os会给用户提供bash和终端,用于给用户提供命令行解析服务,这两个事物结合起来就是一个会话,而在os中我们能够启动多个终端,所以就存在多个会话,所以os需要管理会话,如何管理先描述在组织。也就是使用一个结构体描述会话这个结构体中存在bash和终端的信息。而在命令行中启动的所有的进程都是默认在当前会话内部的一个进程组(单个进程可以自成进程组)。

使用一个图像表示一下:

然后我们能够得到一个信息:任何时刻在一个会话内部,可以存在多个进程组(用户级任务),但是默认任何时刻,只允许一个进程组在前台。

这也是为什么,当我们在启动了sleep 1000之后,在向终端中输入命令这个命令就没有作用了。因为bash也是一个进程组。那么什么是前台呢?

前台也就是和终端/键盘相关,可以接收IO的。所以当bash成为后台之后自然就不能为用户提供命令行解释的服务了

当这么启动就是将进程放到后台:

通过下面的jobs指令能够查看后台进程

如果我想将这个进程放到前台,使用fg 1(后台进程的编号使用jobs可以看到)。

如果现在sleep 1000已经是前台了使用ctrl+z能够自动将这个进程变成后台的进程

当sleep变成后台之后,前台必须存在一个进程,所以当sleep变成后台之后,bash自己就回来了。

jobs---查看后台进程,fg <task_number>将后台变成前台,ctrl+z将前台变成后台。bg<task_number> 也是一样的.

那么这些和进程组有什么关系呢?

首先我们来认识一下什么是用户级任务什么优势进程组。

进程组是一个技术方面的表述,任务是一个用户级的概念,这两个其实一体的。

任务是由用户提出来的,而进程组就是用来完成这些任务的。

这里我们已经知道了每当一个用户登录的时候,都会创建一个会话然后在这个会话中创建bash,以及其它的进程组,当再次登录的时候又会创建一个会话,然后创建bash和其它的进程组。由此我们就知道了会话和会话之间是具有隔离性的。

当我们登录了一个用户之后,就相当于创建了一个会话,然后在这个会话中启动了我们的服务。

当我们启动我们的tcp_server的时候,我们很容易就知道这个服务是启动在当前的会话当中的,这是单个进程所以自成了进程组。

因为这个服务是在用户登录创建的这个会话当中运行的,这也就意味着当用户退出的时候这个服务自然也就停止了。相当于用户退出的时候,这个会话当中的所有东西都会被释放,释放也就意味着,这个服务也就会停止。当然不同的系统对于这一行为的处理方式也是不同的,关键在于我启动的某一个服务是受到某一个用户的启动和关闭的影响的。

所以最后我们想让我们的服务器不受到用户登录和注销的影响。

这也就意味着我们要将我们的服务进程变成一个守护进程。

而在之前的概念中说的任务其实也就是守护进程概念中的作业。而会话则是我们在登录的时候bash给我们启动的一个会话。

在os中进程之间不止存在父子的关系还有同组的关系。

守护进程一定是一个独立的会话。不隶属于任何一个bash的会话。

也就是说在之前我们启动这个我们写的这个服务的时候,无论你是在前台启动这个服务还是将其变成后台的执行,这个服务一定是在用户的这个会话当中的,现在我们要做的就是要将这个服务变成一个守护进程,也就是将这个服务变成一个单独的会话。

要怎么处理呢?

使用的接口如下:

当某一个进程调用了这个系统调用之后就会创建一个单独的会话。并且让这个进程成为这个会话的话首进程。

所以要让我们的这个服务变成守护进程必须要调用这个接口,但是要调用这个接口是存在要求的。

首先介绍一下这个接口的返回值,这个接口在调用成功的时候会返回这个调用成功的进程的pid。如果调用失败返回-1,并且错误码被设置。

所以要求调用这个接口的进程的PID PGID SID都是一样的

还有一个要求:

如果你要使用这个接口创建一个新的会话的话,这个进程不能是进程组中的组长。

因为我们不能是一个组长才能去调用这个接口,所以我们就需要要知道组长是谁。

组长一般都是多个进程中的第一个。

而我们今天写的这个服务,只有一个进程自成进程组,并且自己一定是组长,所以我们一般要创建子进程,让父进程直接退出。此时如果是子进程去执行后面的代码,那么子进程就不是组长了,所以就能够去调用setsid函数了。

所以要让一个进程能够调用这个接口必须要让这个进程不成为组长。

到这里守护进程的理论知识就完成了。

因为要调用上面的接口才能成为守护进程,而要成为守护进程一定需要满足自己不是组长的条件,而要满足这个条件就需要让子进程去执行后面的任务,而让父进程直接退出。由此我们就能知道了:守护进程一定是孤儿进程。所以守护进程的父进程一般都是1,也就是系统。

下面我们就来编写代码:

在编写这个代码的时候

第一步:忽略信号

我们需要忽略可能影响进程异常退出的信号。要忽略的信号由你的应用场景决定。

第二步:不要让自己成为组长

也就是创建进程然后直接让父进程直接退出。

这样这个进程就会被系统接收成为孤儿进程。

第三步就是设置让自己成为一个新的会话(setsid),此时的代码就是子进程再走

然后是第四步:

这里我们需要知道的是每一个进程都有自己的CWD(当前工作路径)守护进程也是有一个策略为是否将当前进程的CWD更改为/根目录。

为什么要将这个CWD变成根目录呢?因为修改之后未来这个服务就能够从根目录开始以绝对路径的方式找到Linux下的和这个服务相关的所有的日志文件,配置文件或者是资源文件。不然就要以当前路径开始寻找这个文件了。

修改路径可以使用chdir接口

然后是第五步

到达第五步的时候进程已经变成守护进程了,也就是已经是一个独立的会话了。也就不需要和用户的输入输出,错误进行关联了。

当然可以选择close(0),close(1),close(2)这样的代码去关闭。但是这种方案不友好。因为在主代码中可能真的存在printf/scanf这样的函数,此时向一个已经关闭了的文件描述符中写信息,就会直接异常让os直接将这个进程kill了。所以一般不使用这样的代码。

这里推荐的方式和一个文件有关这个文件在下面的路径:

任何一款Linux系统都会提供一个这样的字符设备。凡是往这个字符设备写的任何东西,全部自动会被丢弃。凡是想从这个文件中读取的进程,自动读到文件的结尾。也即是这个字符文件的特点就是抛弃一切,所以比较好的做法就是:

打开这个文件,然后dup替换012为这个文件,最后关闭fd即可。

这个dup2也就是将fd文件描述符中的内容覆盖拷贝到文件描述符表中012的内容就完成了重定向。到这里也就完成了守护进程化的最重要的5个步骤就完成了。当然也还有其它的方法去完成守护进程化,但是最重要的五个步骤就是上面说的这5个。

当然这里你也可以提供一个方式,这种方式就是直接关闭012,而不是进行重定向。

下面我们来测试一下:

需要注意守护进程化的形成的进程的名字一般都以d为结尾。、

当启动之后我们是感受不到的,但是使用ps去查看可以看到我们刚刚的服务进程,确实已经是一个孤儿进程了

同时pid pgid和sid确实也是一样的。并且当我重新登录的时候这个服务还是存在的。

至于如何关闭这个进程呢?直接使用kill -9指令即可。

现在如果我将这个代码修改为要改变CWD,再去编译一下查看是否修改成功呢?

可以看到这个守护线程的cwd确实被修改为了/目录。

那么系统有没有提供将进程守护进程化的方法呢?

我们刚刚写的TCPserver和UDPserver要如何守护进程化呢?

提供的接口:

至于这两个参数自然就是是否选择修改根目录,和是否重定向到/dev/null文件上。

这里将012重定向到null文件上是最好的选择。

下面来将我的服务守护进程化。

这里推荐守护进程放在创建服务器之前

然后就来测试一下:

当我启动服务器之后,可以看到我的bash依旧是在运行的。

同时这也是为什么我们写的这个日志要具有往文件中打印的能力。

这样就完成了将我们的服务守护进程化。并且这个进程也能正常的运行了。

并且日志也有了。

此时就算我将bash退出了这个服务也一直会在运行。

这也是为什么要存在云服务。有了云服务(24小时不关机),才能一直提供服务。

希望这篇文章能对您有所帮助,非常感谢您的阅读,如果发现了任何的错误欢迎指出,写的不好请见谅。如果您需要源码,请私信我。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/598905.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

护眼台灯品牌排行前十名有哪些?最新护眼台灯品牌排行前十名分享

在近几年&#xff0c;一个引人关注的健康现象是青少年近视问题的日益加剧。统计数字显示&#xff0c;近视的发病率不断攀升&#xff0c;令人忧心地发现&#xff0c;许多才刚步入小学一年级的孩子们&#xff0c;便已佩戴起了厚重的眼镜。其中最主要的原因就在于学习过程对数码设…

高实时、高可靠的微内核操作系统——鸿道Intewell

近年来&#xff0c;我国不断推进工业转型升级&#xff0c;力求实现从传统工业大国向现代工业强国的跨越。想要在新一轮科技革命中“超车”&#xff0c;需要从多个维度进行深度布局和全面发力。 ——科技创新是核心驱动力 积极推动工业结构的优化和升级&#xff0c;通过发展新…

鸿蒙OpenHarmony南向:【Hi3516标准系统入门(IDE方式)】

Hi3516标准系统入门&#xff08;IDE方式&#xff09; 注意&#xff1a; 从3.2版本起&#xff0c;标准系统不再针对Hi3516DV300进行适配验证&#xff0c;建议您使用RK3568进行标准系统的设备开发。 如您仍然需要使用Hi3516DV300进行标准系统相关开发操作&#xff0c;则可能会出现…

Windows设置Redis为开机自启动

前言 Redis作为当前最常用的当前缓存技术&#xff0c;基本上Web应用中都有使用。所以&#xff0c;每次我们在本地启动项目前&#xff0c;都必须将Redis服务端启动。但是&#xff0c;每次都要去启动Redis就很麻烦&#xff0c;有没有办法做到开机自动启动Redis呢&#xff1f;这当…

leetCode69. x 的平方根

leetCode69. x 的平方根 题目思路 常见的二分法模板&#xff08;背过就行&#xff0c;模板而已&#xff09; // 区间[L,R]被划分为[L,mid]和[mid 1, R]时使用这个模板 int bsearch_1(int l, int r){while(l < r){int mid l r >> 1;if(check(mid)) r mid; //che…

CoPilot 产品体验:提升 OpenNJet 的控制管理和服务提供能力

文章目录 前言系统架构介绍CoPilot 配置CoPilot 插件规范 体验 CoPilot 实例CoPilot: Broker 实例CoPilot: Ctrl 实例 开发其他语言编写的 CoPilot目标主要思路具体实现执行 go 程序代码 功能扩展总结 前言 CoPilot 是 OpenNJet 的一个重要组成部分&#xff0c;它在 Master-Wo…

我独自升级崛起怎么下载 我独自升级崛起下载教程来了

《我独自升级&#xff1a;崛起》作为一款炙手可热的游戏&#xff0c;其非凡的品质迅速聚拢了大量玩家的目光&#xff0c;就如同磁铁吸引铁屑一般&#xff0c;展现了优质游戏所固有的强大吸引力。在这款游戏中&#xff0c;每位玩家都能化身成为拥有超凡能力的英雄&#xff0c;体…

物联网技术、测试要点和测试标准

物联网定义 物联网&#xff1a;利用嵌入式电子设备、微芯片等连接车辆、家电、医疗设备&#xff0c;以收集和交换不同类型的数据&#xff0c;被称为物联网。借助物联网&#xff0c;用户能够远程控制设备&#xff0c;可以实现不同设备的互联。在现实生活中物联网开始有越来越多…

使用开放式用户通信连接两台西门子S71200plc

步骤1.在项目中创建两台PLC。 步骤2.分别设置两个PLC的参数。 plc1 plc2 步骤3.对两个plc进行组态 步骤4.在plc1和plc2中各自创建DB块&#xff0c;用于通信。 须在块的属性中取消优化块的访问选项。 plc1 plc2 步骤5.往plc1的main块中编写代码。 步骤6.往plc2的main块中编写…

Python爬虫--Urllib基础

1. urlretrieve Urllib 库也是类似 request 库&#xff0c;用来解析html的 首先讲 urlretrieve 子模块 这个模块的作用是将网页下载到本地 语法&#xff1a; urlretrieve(网址,本地地址) 例如&#xff1a; 这样就可以了&#xff0c;他会将百度网页下载到本地D盘下&#x…

【iOS】——浅析CALayer

文章目录 一、CALayer介绍二、UIview与CALayer1.区别2.联系 三、CALayer的使用1.初始化方法2.常用属性 四.CALayer坐标系1.position属性和anchorPoint属性2.position和anchorPoint的关系3.position、anchorPoint和frame的关系 五、CALayerDelegate六、CALayer绘图机制1.绘图流程…

AGI|基于LangChain实现的三种高级RAG检索方法

一、前言 RAG(Retrieval-Augmented Generation)检索增强生成&#xff0c;是现如今基于企业私域知识的问答应用所使用的主流技术之一。相较于重新训练基于私域知识的大模型来说&#xff0c;RAG没有额外的预训练成本&#xff0c;且回答效果与之相当。 但在实际应用场景中&#xf…

容器Docker:轻量级虚拟化技术解析

引言 随着云计算和虚拟化技术的飞速发展&#xff0c;容器技术以其轻量级、高效、可移植的特性&#xff0c;逐渐成为了软件开发和部署的新宠。在众多容器技术中&#xff0c;Docker以其简单易用、功能强大的特点&#xff0c;赢得了广泛的关注和应用。本文将全面介绍Docker的基本概…

数据挖掘算法原理与实践:决策树

第2关&#xff1a;决策树算法原理 任务描述 本关任务&#xff1a;根据本关所学知识&#xff0c;完成 calcInfoGain 函数。 相关知识 为了完成本关任务&#xff0c;你需要掌握&#xff1a; 信息熵&#xff1b;条件熵&#xff1b;信息增益。 信息熵 信息是个很抽象的概念。…

04-xss获取cookie实验

二、开发XSS服务器端 1、确认实验环境 攻击者服务器&#xff1a;192.168.74.134&#xff0c;将获取到cookie数据保存到该服务器的数据库中&#xff0c;运行PHP代码暴露一个接收Cookie的URL地址。 正常Web服务器&#xff1a;192.168.74.133&#xff0c;用于正常的用户访问的目…

Linux磁盘IO、网络IO、零拷贝详解

一、什么是I/O&#xff1f; 在计算机操作系统中&#xff0c;所谓的I/O就是输入&#xff08;input&#xff09;和输出&#xff08;output&#xff09;,也可以理解为读&#xff08;read&#xff09;和写&#xff08;write&#xff09;,针对不同的对象&#xff0c;I/O模式可以划分…

46. UE5 RPG 实现角色死亡效果

在上一篇文章中&#xff0c;我们实现了敌人受到攻击后会播放受击动画&#xff0c;并且还给角色设置了受击标签。并在角色受击时&#xff0c;在角色身上挂上受击标签&#xff0c;在c里&#xff0c;如果挂载了此标签&#xff0c;速度将降为0 。 受击有了&#xff0c;接下来我们将…

Compose 状态管理

文章目录 Compose 状态管理概述使用MutableStaterememberStatelessComposable & StatefulComposable状态提升rememberSaveable支持parceable不支持parceable 使用ViewModelViewModelProvider.Factory 使用Flow Compose 状态管理 概述 当应用程序的状态发生变化时&#xf…

如何与精益生产咨询公司合作,确保项目的成功?

随着竞争的白热化&#xff0c;企业为了提升生产效率和降低成本&#xff0c;纷纷寻求精益生产咨询公司的帮助。然而&#xff0c;与咨询公司合作并不是一蹴而就的事情&#xff0c;需要双方共同努力&#xff0c;才能确保项目的成功。那么&#xff0c;如何与精益生产咨询公司合作&a…
最新文章