网络编程、tcp、守护进程、前台和后台任务、bash 与 shell、会话
上篇,我们讲解了udp服务器与客户端的功能,这篇我们将使用tcp协议来进行编程;tcp服务器相比较与udp要更加稳定与安全,tcp服务器是面向连接的数据传输;
1. tcp服务器与客户端
下面是我实现的完整代码可以辅助下面的讲解理解:
network_code/socket_2024_9_17/4_tcp · future/Linux - 码云 - 开源中国 (gitee.com)
下面我将对其值得注意的地方进行讲解;
1.1 socket套接字接口
socket与bind
由于这两个接口在上一篇就有讲过了,所以这里只是说明一下,证明他们的使用;
1.1.1 listen
这个函数是服务器用来监听socket套接字的接口,socket套接字通过上面的socket函数创建,socket函数创建好socket套接字之后,我们需要将这个绑定了本进程信息的套接字通过listen函数设置为listensock套接字,设置成功后这个listen函数会返回一个listensock,这个listensock负责监听底层有无其他进程向服务器进程进行访问,如果有进程通过网络来访问服务器进程,由于tcp协议是面向连接的,在底层中服务端会向客户端回信标识自己接收到了访问,这个listensock就是在底层进行回信的套接字,但是我们是不清楚他内部是如何实现的;
上面大致的讲解了listen接口做了什么下面我们来讲讲这个接口的使用;
1.这个接口的返回值是一个套接字也可以看作是封装了sock的listensock网卡文件接口,之后服务器的工作就是通过listensock来实现的,当listen失败时会返回-1,且设置错误码errno;
2.函数的第一个参数是我们使用socket函数创建的本线程的socket套接字;
3.函数的第二个参数是一个回文,这个回文暂时不做讲解,现在只需要知道数值大小不需要设置太大,设置在5到10之间即可
1.1.2 connect
这个函数一般是客户端用来连接服务器的,因为服务器一般是被动接收很多不同客户端发送来的信息的,所以需要客户端先发送连接请求,而tcp协议在通信是是需要建立连接的(udp前面的操作就是直接通信,没有建立连接),而这个connect函数就是用来建立连接的函数,我们知道上面的listen函数设置了套接字来监听有什么客户端进程对服务器发起了访问;connnet函数就如同发起访问的函数一般,告诉服务器,我要来访问你了,你收到的话,请给我回信告诉我你收到了,如果客户端收到服务端接收成功的回信,connect就会返回0,说明建立连接成功,可以向服务端发送信息了,此时connect的底层其实也会发送信息给服务端,告诉服务端我收到了你的回信,但底层的实现我们也还是不清楚的;
接下来我们继续介绍函数的使用;
1.connect的返回值建立连接成功返回0失败返回-1并设置errno错误码
2.connect的第一个参数是客户端所创建的套接字
3.connect的第二个和第三个参数是要访问的服务器的ip地址与端口号
1.1.3 accept
上面的两个函数都是服务器用来接收客户端回信的函数,accept4功能比accept多一个flag的设置功能,我们暂时先不管他即可;上面说到connect函数向服务器回信告诉服务器连接已经建立成功我们可以开始通信了,此时由于listensock是用来监听访问的,无法再为客户端的请求服务,所以需要通过accept函数来接收客户端回信,还要分配新的socket来对客户端的请求服务;accept的底层会创建出新的套接字作为返回值,提供给程序员;
1.accept的返回值是一个sockfd这个sockfd是用来为客户端提供服务的sock,我们可以从这个网卡文件描述符中获取客户端发来的信息,也可以向其中返回我们的数据,和普通文件描述符一般使用即可,返回值为-1表示接收出现错误,并设置errno;
2.accept的第1个参数是监听套接字,用来获取监听到的来访问的客户端
3.accept的第2和第3个参数是输出型参数可以获取访问的客户端的信息的sockaddr结构体与结构体大小;
1.2 telnet连接命令
通过上面的接口介绍,我们再联系我代码的实现就可以基本的写出tcp客户端和服务器的大致功能,接下来呢介绍一个linux下现成的工具telnet,使用方法:
telnet (ip地址) (端口号)
例:telnet 127.0.0.1 10000
这个小工具可以完成对服务器发送信息,并接收服务器返回数据的功能;
1.3 发送信息转换网络字节序转换
我们再bind和获取sockaddr信息时都需要将数据进行网络字节序的转换,那为什么我们发送到网络中的信息不需要进行网络字节序的转换呢?其实我们不需要纠结这些,既然我们能够做到信息的成功传输那么就说明,再socket套接字的底层一定是对信息做了处理,使得信息在网络中的传输是符合网络字节序的;而例如sin_addr和sin_port这样的信息是我们再用户层写的,要传入内核中的数据,这样的特例需要我们进行显式的修改而已;
2.tcp服务器服务实现方式
我们对tcp服务器有几种不同的实现方式:
1.单进程,无法做到同时为多个客户端服务
serverce(socketfd, userIp, userPort);
close(socketfd);
2.多进程
pid_t pid = fork();
if (pid == 0)
{
// 子进程
close(_socketListen);
if (fork() != 0)
exit(-1);
serverce(socketfd, userIp, userPort);
close(socketfd);
exit(-1); // 孙子进程也要记得退出哦
}
父进程
close(socketfd);
waitpid(pid, nullptr, 0);
3.多进程 使用signal函数进行等待,父进程不需要阻塞等待
pid_t pid = fork();
if (pid == 0)
{
// 子进程
close(_socketListen);
serverce(socketfd, userIp, userPort);
close(socketfd);
exit(-1);//子进程要记得退出哦
}
// 父进程
close(socketfd);
4.进程池,在run函数最开始就创建多个进程,进程竞争的接收
客户端,但是需要注意对与socket的accept需要加锁访问
5.多线程
pthread_t tid;
threadData* data= new threadData(socketfd, userIp, userPort, this);
pthread_create(&tid, 0, routine, (void *)data);
6.线程池
task t = task(socketfd, userIp, userPort);
threadPool<task>::getThreadPool()->push(t);
这些实现方式是层层递进的,我们一开始使用的是单进程方式,但是由于tcp是面向连接的连接的用户,一个进程只能服务一个客户端所以,这样的服务器是不合格的;——>转变为了多进程的服务,但是虽然多进程服务,进程创建的消耗太大了;——>转变为了多线程服务,多线程消耗小,但是线程的创建也是有代价的;——>最后将服务设计成多线程的服务,并且限制线程数量,用线程池来跑,再把服务修改为短服务,这样使得服务器可以长久的运行;如何实现可以通过上面的代码来了解,这里就不多做讲解了;
3.tcp客户端的断连重连功能
我们将服务器的功能基本完善后,由于将服务器设置为了短服务,所以我们的客户端想要连接服务器需要不断重新accept去连接服务器,那么这样的过程我们还可以丰富一下再加上重复连接的功能,使得客户端的模型更加贴近我们现实生活中的场景:
会让服务器的功能大致成为上面这样;
我们可以看看我们服务器的重连现象:
我们只需要看到,我们是可以实现重连功能的,至于我上面实现的是什么样的服务可以先不用了解,上面实现的服务一个翻译功能,我们输入英文,其可以翻译中文给我们;在我们打王者荣耀的时候,其实我们掉线了其实也是有这个重连功能的:
其实重连功能基本都是这样哒!
3.1 setcocketopt接口
我们实现的服务器在重新启动时有可能会存在端口号被占用无法马上重新启动服务器的情况这个时候,我们只需要使用这个接口设置一下即可解决此问题:
// 下面的代码用来让服务器重启不需要等待时间,暂时不知道原理,先用着
int opt = 1;
setsockopt(_socketListen, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
4.tcp服务器翻译功能的实现
下面是代码实现,其实在我上面的gitee链接中也有,这里拿出来更方便观看
#pragma once
#include <iostream>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#include "log.hpp"
#include <cstring>
#include <unistd.h>
#include <unordered_map>
#include <fstream>
class dict
{
public:
void Init()
{
ifstream in("./dict.txt");
string str;
while (getline(in, str))
{
string key, value;
auto pos = str.find(':');
if (pos == string::npos)
continue;
key = str.substr(0, pos);
value = str.substr(pos + 1);
_dict.insert({key, value});
}
}
static dict *getdict()
{
// 这里因为是只读,不会对其修改,所以不需要上锁
if (_self == nullptr)
{
_self = new dict;
}
return _self;
}
unordered_map<string, string> _dict;
private:
dict() {}
dict(const dict &) = delete;
dict &operator=(const dict &) = delete;
static dict *_self;
};
dict* dict::_self = nullptr;
dict.txt文件:
yellow:黄色
red:红色
pig:猪
cat:猫
dog:狗
通过将一个字典载入服务器中,让服务器可以通过这个字典的键值对获取相应的翻译:
for(auto word:dict::getdict()->_dict)
{
if(word.first==getMsg)
sendInfo+=word.second;
}
if(sendInfo.size()==0)
sendInfo="unkonw";
5.守护进程化
在我们现实生活中,我们使用的app如王者荣耀,抖音,这些app无论我们什么时候,我们只要联网了就可以使用这些软件,所以这些软件提供的服务都是24小时的,那么他们的服务器也一定是24小时运行的;而如果想要一个进程每天24小时运行,那么这个进程一定不能被误杀并且不能出现让进程死亡的bug,那么我们怎么样才能做到呢?接下来我们就通过守护进程化来做到;首先我们先讲解一下,前后台任务的概念:
5.1 前台后台任务
5.1.1 什么是会话与前后台任务与bash和shell
首先我们需要介绍一个名词——会话,在我们使用电脑时,电脑也可以创建多个用户,每个不同的用户登录上电脑就会生成不同的会话。而我们云服务器在登录远程主机时每次登录都会创建一个会话,我们可以这样理解:
对于我们的会话,我们可以这么理解,会话是一个用户在使用主机时所产生的与主机交互的一个实体,我们运行的进程还有需要的数据都会显示在会话上,而大多数时刻一个会话在一个时刻是只能显示一个页面的,那么在这个页面上显示的就是我们的前台进程(任务),所以一个会话只能在一个时刻存在一个前台进程(任务)
而在我们的linux云服务器上我们可以通过xshell对其进行远程连接,我们每建立一次连接,就是在远程的云服务器主机上建立了一个会话session,而这些每个不同的会话都会有他们自己的唯一的前台程序(任务),而bash就是linux下的一个默认的前台程序(任务),他会将我们标准输入(键盘)上的数据进行解析,运行出我们想要执行的指令;
Bash(全称为 "Bourne Again SHell")是一个命令行解释器;
shell是用来与操作系统进行交互的接口,他有bash还有图形化界面多种表示方式
我们下面使用grep查看也可以查看到bash默认前台任务:
那么什么样的进程(任务)会被称前台程序(任务)呢?
我们可以将拥有标准输入流的进程当作前台程序,在windows这样带有图形化界面的程序上不仅仅是键盘还有鼠标拥有这些输入的就是前台程序(任务);
5.1.2 前后台任务切换指令
我们理解了什么是前后台任务后,在linux下我们可以使用一些命令让我们的任务进行前后台的切换:
1.将前台任务置换为后台任务指令:
./进程名 & 例如: ./a.out &
我们有一个这样的程序:
当我们使用&运行它时:
这个程序这样运行我们无法使用ctrl+将其终止,因为我们ctrl+c会发送信号给前台进程,而这里的test是后台进程了,所以也证明了只有前台程序才拥有标准输入流(键盘);
2.查看后台程进程令命令
jobs
3.将后台进程提到前台命令
fg 任务号 例如: fg 1
4.将后台暂停程序重新启动
我们可以使用ctrl+z让前台运行的程序暂停,bash会自动被置换回前台
bg 任务号
5.1.3 任务与进程组
有事一个任务会分配给一个进程组,而一个进程组中可能有多个进程,例如fork创建的进程,这些进程在一个组共同处理任务,所以我们也叫这些任务叫前后台任务,例如我们windows下的编译器,就是一个个进程组来共同处理任务:
看上面的vs code进程组的例子;
任务是指派给进程组的,只不过有些进程组只有一个进程罢了
5.2 实现守护进程化
上面我们铺垫了这么久,我们究竟如何做到守护进程化呢?
我们需要分这几步:
void daemon()
{
if(fork()!=0)exit(-1);//父进程直接退出
//忽略信号,让信号不会误杀进程
signal(SIGPIPE,SIG_IGN);
signal(SIGCLD,SIG_IGN);
signal(SIGSTOP,SIG_IGN);
setsid();//自成会话
//更改pwd
//chdir("/");
//重定向012fd到/dev/null 堵不如疏
int fd=open("/dev/null",O_RDWR);
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
}
1.srtsid(),这个函数是用来让进程自成会话的,使得一个终端退出时,让我们的进程不会被终端的会话影响而退出,让服务器成为独立的会话中的进程,也叫自成会话;
2.让父进程退出创建子进程的原因是,我们进程组的组长不允许自成会话,所以创建出子进程,让子进程执行代码,父进程组长退出,子进程成为孤儿进程让系统领养即可
3.我们还可以修改pwd,进程的运行目录,防止被同一目录下的其他进程影响
4.最后关闭所有输出,让输出全部输出到/dev/null文件下,这个文件是专门用来提供给程序员输出不需要信息的,需要的log日志信息会通过log代码输入到我们需要的文件中;
通过这些操作,就可以让我们的进程稳定在后台运行;
其实库中提供给了我们守护线程的函数,只不过一般这个守护进程化都是我们程序员自己实现的:
第一个参数为是否修改pwd路径到"/"根目录,第二个参数为是否重定向0,1,2fd文件描述符到/dev/null文件中;
6.tcp协议过程
我们前面说了这么多函数,接下来我们来总结一下tcp协议的通信过程:
就是通过上面三次握手的过程建立了连接,其实这三次握手实际的操作,我们是看不到的都是底层的套接字在做,我们只需要调用接口就好了;四次挥手也是同理有底层套接字完成;
因为是面向连接的通信,所以服务器端会有很多个套接字和客户端连接,套接字也是通过先描述后组织的方式被管理起来的,就是说服务客户端的套接字会在服务端形成一个数据结构被管理起来;
以上便是tcp协议通信的过程;
推荐阅读
-
网络编程、tcp、守护进程、前台和后台任务、bash 与 shell、会话
-
windows下进程间通信的(13种方法)-摘 要 本文讨论了进程间通信与应用程序间通信的含义及相应的实现技术,并对这些技术的原理、特性等进行了深入的分析和比较。 ---- 关键词 信号 管道 消息队列 共享存储段 信号灯 远程过程调用 Socket套接字 MQSeries 1 引言 ---- 进程间通信的主要目的是实现同一计算机系统内部的相互协作的进程之间的数据共享与信息交换,由于这些进程处于同一软件和硬件环境下,利用操作系统提供的的编程接口,用户可以方便地在程序中实现这种通信;应用程序间通信的主要目的是实现不同计算机系统中的相互协作的应用程序之间的数据共享与信息交换,由于应用程序分别运行在不同计算机系统中,它们之间要通过网络之间的协议才能实现数据共享与信息交换。进程间通信和应用程序间通信及相应的实现技术有许多相同之处,也各有自己的特色。即使是同一类型的通信也有多种的实现方法,以适应不同情况的需要。 ---- 为了充分认识和掌握这两种通信及相应的实现技术,本文将就以下几个方面对这两种通信进行深入的讨论:问题的由来、解决问题的策略和方法、每种方法的工作原理和实现、每种实现方法的特点和适用的范围等。 2 进程间的通信及其实现技术 ---- 用户提交给计算机的任务最终都是通过一个个的进程来完成的。在一组并发进程中的任何两个进程之间,如果都不存在公共变量,则称该组进程为不相交的。在不相交的进程组中,每个进程都独立于其它进程,它的运行环境与顺序程序一样,而且它的运行环境也不为别的进程所改变。运行的结果是确定的,不会发生与时间相关的错误。 ---- 但是,在实际中,并发进程的各个进程之间并不是完全互相独立的,它们之间往往存在着相互制约的关系。进程之间的相互制约关系表现为两种方式: ---- (1) 间接相互制约:共享CPU ---- (2) 直接相互制约:竞争和协作 ---- 竞争——进程对共享资源的竞争。为保证进程互斥地访问共享资源,各进程必须互斥地进入各自的临界段。 ---- 协作——进程之间交换数据。为完成一个共同任务而同时运行的一组进程称为同组进程,它们之间必须交换数据,以达到协作完成任务的目的,交换数据可以通知对方可以做某事或者委托对方做某事。 ---- 共享CPU问题由操作系统的进程调度来实现,进程间的竞争和协作由进程间的通信来完成。进程间的通信一般由操作系统提供编程接口,由程序员在程序中实现。UNIX在这个方面可以说最具特色,它提供了一整套进程间的数据共享与信息交换的处理方法——进程通信机制(IPC)。因此,我们就以UNIX为例来分析进程间通信的各种实现技术。 ---- 在UNIX中,文件(File)、信号(Signal)、无名管道(Unnamed Pipes)、有名管道(FIFOs)是传统IPC功能;新的IPC功能包括消息队列(Message queues)、共享存储段(Shared memory segment)和信号灯(Semapores)。 ---- (1) 信号 ---- 信号机制是UNIX为进程中断处理而设置的。它只是一组预定义的值,因此不能用于信息交换,仅用于进程中断控制。例如在发生浮点错、非法内存访问、执行无效指令、某些按键(如ctrl-c、del等)等都会产生一个信号,操作系统就会调用有关的系统调用或用户定义的处理过程来处理。 ---- 信号处理的系统调用是signal,调用形式是: ---- signal(signalno,action) ---- 其中,signalno是规定信号编号的值,action指明当特定的信号发生时所执行的动作。 ---- (2) 无名管道和有名管道 ---- 无名管道实际上是内存中的一个临时存储区,它由系统安全控制,并且独立于创建它的进程的内存区。管道对数据采用先进先出方式管理,并严格按顺序操作,例如不能对管道进行搜索,管道中的信息只能读一次。 ---- 无名管道只能用于两个相互协作的进程之间的通信,并且访问无名管道的进程必须有共同的祖先。 ---- 系统提供了许多标准管道库函数,如: pipe——打开一个可以读写的管道; close——关闭相应的管道; read——从管道中读取字符; write——向管道中写入字符; ---- 有名管道的操作和无名管道类似,不同的地方在于使用有名管道的进程不需要具有共同的祖先,其它进程,只要知道该管道的名字,就可以访问它。管道非常适合进程之间快速交换信息。 ---- (3) 消息队列(MQ) ---- 消息队列是内存中独立于生成它的进程的一段存储区,一旦创建消息队列,任何进程,只要具有正确的的访问权限,都可以访问消息队列,消息队列非常适合于在进程间交换短信息。 ---- 消息队列的每条消息由类型编号来分类,这样接收进程可以选择读取特定的消息类型——这一点与管道不同。消息队列在创建后将一直存在,直到使用msgctl系统调用或iqcrm -q命令删除它为止。 ---- 系统提供了许多有关创建、使用和管理消息队列的系统调用,如: ---- int msgget(key,flag)——创建一个具有flag权限的MQ及其相应的结构,并返回一个唯一的正整数msqid(MQ的标识符); ---- int msgsnd(msqid,msgp,msgsz,msgtyp,flag)——向队列中发送信息; ---- int msgrcv(msqid,cmd,buf)——从队列中接收信息; ---- int msgctl(msqid,cmd,buf)——对MQ的控制操作; ---- (4) 共享存储段(SM) ---- 共享存储段是主存的一部分,它由一个或多个独立的进程共享。各进程的数据段与共享存储段相关联,对每个进程来说,共享存储段有不同的虚拟地址。系统提供的有关SM的系统调用有: ---- int shmget(key,size,flag)——创建大小为size的SM段,其相应的数据结构名为key,并返回共享内存区的标识符shmid; ---- char shmat(shmid,address,flag)——将当前进程数据段的地址赋给shmget所返回的名为shmid的SM段; ---- int shmdr(address)——从进程地址空间删除SM段; ---- int shmctl (shmid,cmd,buf)——对SM的控制操作; ---- SM的大小只受主存限制,SM段的访问及进程间的信息交换可以通过同步读写来完成。同步通常由信号灯来实现。SM非常适合进程之间大量数据的共享。 ---- (5) 信号灯 ---- 在UNIX中,信号灯是一组进程共享的数据结构,当几个进程竞争同一资源时(文件、共享内存或消息队列等),它们的操作便由信号灯来同步,以防止互相干扰。 ---- 信号灯保证了某一时刻只有一个进程访问某一临界资源,所有请求该资源的其它进程都将被挂起,一旦该资源得到释放,系统才允许其它进程访问该资源。信号灯通常配对使用,以便实现资源的加锁和解锁。 ---- 进程间通信的实现技术的特点是:操作系统提供实现机制和编程接口,由用户在程序中实现,保证进程间可以进行快速的信息交换和大量数据的共享。但是,上述方式主要适合在同一台计算机系统内部的进程之间的通信。 3 应用程序间的通信及其实现技术 ---- 同进程之间的相互制约一样,不同的应用程序之间也存在竞争和协作的关系。UNIX操作系统也提供一些可用于应用程序之间实现数据共享与信息交换的编程接口,程序员可以通过自己编程来实现。如远程过程调用和基于TCP/IP协议的套接字(Socket)编程。但是,相对普通程序员来说,它们涉及的技术比较深,编程也比较复杂,实现起来困难较大。 ---- 于是,一种新的技术应运而生——通过将有关通信的细节完全掩盖在某个独立软件内部,即底层的通讯工作和相应的维护管理工作由该软件内部来实现,用户只需要将通信任务提交给该软件去完成,而不必理会它的具体工作过程——这就是所谓的中间件技术。 ---- 我们在这里分别讨论这三种常用的应用程序间通信的实现技术——远程过程调用、会话编程技术和MQSeries消息队列技术。其中远程过程调用和会话编程属于比较低级的方式,程序员参与的程度较深,而MQSeries消息队列则属于比较高级的方式,即中间件方式,程序员参与的程度较浅。 ---- 4.1 远程过程调用(RPC)