文章目录
简单TCP服务器的实现
- TCP区别于UDP在于要设置套接字为监控状态,即TCP是面向链接,因此TCP套接字需要设置为监听状态
void initserver(){//1.创建套接字_listensock=socket(AF_INET,SOCK_STREAM,0);if(_listensock<0){ logMessage(FATAL,"create listensocket error"); exit(SOCK_ERR);} logMessage(NORMAL, "create socket success: %d", _listensock);//2.bind ip和portstruct sockaddr_in local;local.sin_family=AF_INET;local.sin_port=htons(_port);local.sin_addr.s_addr=INADDR_ANY;if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//绑定失败{ logMessage(FATAL,"bind error"); exit(BIND_ERR);} logMessage(NORMAL,"bind success");//3.将套接字设置为监听模式if(listen(_listensock,0)<0){ logMessage(FATAL,"listen error"); exit(LISTEN_ERR);}logMessage(NORMAL,"listen success");}
socket函数原型
#include #include int socket(int domain, int type, int protocol);
-
domain
表示协议族,常用的有AF_INET
(IPv4)和AF_INET6
(IPv6)。 -
type
表示Socket类型,常用的有SOCK_STREAM
(TCP)和SOCK_DGRAM
(UDP)。 -
protocol
通常可以设置为 0,让系统根据domain
和type
来选择合适的协议。 -
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符
-
应用程序可以像读写文件一样通过socket函数用read/write在网络上收发数据
bind函数原型
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
sockfd
是socket描述符。 -
addr
是一个struct sockaddr
结构体,包含要绑定的IP地址和端口信息。 -
addrlen
是addr
结构体的长度。因为addr结构体可以接受多种协议的sockaddr结构体,因此要传其结构体的长度 -
bind()成功返回0,失败返回-1。
-
bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号;
listen函数原型
int listen(int sockfd, int backlog);
sockfd
是socket描述符,指用于进行网络监听的文件描述符backlog
表示等待连接队列的最大长度。- listen成功返回0,失败返回-1
- listen函数将使得sockfd处于监听状态,并且允许backlog个客户端处于连接等待状态,当收到多于backlog个客户端的的连接请求则选择忽略。
- 实际上listen函数告诉操作系统指定的套接字sockfd处于监听状态,该套接字开始等待其他计算机通过网络与其建立连接,一旦有连接请求到达,操作系统会将连接请求放入连接队列中,连接队列的最大长度为backlog,连接队列是一个存放连接请求的缓冲区,如果队列已满新的连接请求将会被拒绝。即当一个套接字处于监听状态时,它不直接处理数据传输,而是等待其他计算机发起连接。
总的来说initserver函数作用是先创建套接字,然后填充指定的端口号和ip,并将套接字设置为监听状态
void start(){ while(true) { struct sockaddr_in cli; socklen_t len=sizeof(cli); bzero(&cli,len); int sock=accept(_listensock,(struct sockaddr*)&cli,&len); if(sock<0) { logMessage(FATAL,"accept client error"); continue; } logMessage(NORMAL,"accept client success"); cout<<"accept sock: "<<sock<<endl; }
accept函数原型
#include #include int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-
sockfd
:是一个已经通过socket
函数创建的套接字描述符,并且是已经处于监听状态,用于监听传入的连接请求。 -
addr
:是一个指向struct sockaddr
结构的指针,用于接收连接请求的客户端的地址信息。 -
addrlen
:是一个指向socklen_t
类型的指针,用于指定addr
缓冲区的长度,同时也用于返回实际客户端地址结构的大小。 -
accept函数作用是接受传入的连接请求,他会阻塞程序的执行,直到有一个连接请求到达。一旦有连接请求到达,将会创建一个新的套接字,并返回这个新套接字的文件描述符,这个新套接字用于与客户端进行通信,同时
addr
和addrlen
会填充上客户端的地址信息。 -
在服务器程序中,accept函数会被用在一个循环中,以接受多个客户端的连接请求
start函数作用是阻塞接受客户端发送来的连接请求,使得服务器与客户端建立通信
tcpclient.cc
#include #include #include #include"tcpclient.hpp"using namespace std;using namespace client;static void Usage(string proc){ cout<<"\nUsage :\n\t"<<proc<<" serverip serverport\n"<<endl;}int main(int argc, char* argv[]){ if(argc!=3) { Usage(argv[0]); exit(1); }string serverip=argv[1];uint16_t serverport=atoi(argv[2]);unique_ptr<tcpclient> tc(new tcpclient(serverip,serverport));tc->initclient();tc->start(); return 0;}
tcpclient.hpp
#pragma once#include #include #include #include #include #include #include #include using namespace std;#define NUM 1024namespace client{ class tcpclient{public:tcpclient(const string& ip,const uint16_t& port):_sock(-1),_port(port),_ip(ip){}void initclient(){//1.创建sockfd_sock=socket(AF_INET,SOCK_STREAM,0);if(_sock<0){ cerr<<"socket create error"<<endl; exit(2);}//2.绑定 ip port,不显示绑定,OS自动绑定}void start(){struct sockaddr_in ser;bzero(&ser,sizeof(ser));socklen_t len=sizeof(ser);ser.sin_family=AF_INET;ser.sin_port=htons(_port);ser.sin_addr.s_addr=inet_addr(_ip.c_str());if(connect(_sock,(struct sockaddr *)&ser,len)!=0){ cerr<<"connect error"<<endl;}else{ string msg; while(true) { cout<<"Enter# "; getline(cin,msg); write(_sock,msg.c_str(),msg.size()); char inbuffer[NUM]; int n=read(_sock,inbuffer,sizeof(inbuffer)-1); if(n>0) { cout<<"server return :"<<inbuffer<<endl; }else { break; } }}}~tcpclient(){ if(_sock>=0) close(_sock);}private:int _sock;uint16_t _port;string _ip;};}
tcpserver.cc
#include"tcpserver.hpp"#include"log.hpp"#include #include #include using namespace Server;using namespace std;static void Usage(string proc){ cout<<"\nUsage:\n\t"<<proc<<" local_port\n\n"<<endl;}int main(int argc,char* argv[]){if(argc!=2){ Usage(argv[0]); exit(USAGE_ERR);}uint16_t port=atoi(argv[1]);//将字符串转化为整数unique_ptr<tcpserver> ts(new tcpserver(port));ts->initserver();ts->start();return 0;}
tcpserver.hpp
#pragma once#include #include #include #include #include #include #include #include #include #include #include #include #include"log.hpp"#define NUM 1024using namespace std;namespace Server{ enum { USAGE_ERR=1,SOCK_ERR,BIND_ERR,LISTEN_ERR };class tcpserver; class ThreadData {public:ThreadData( tcpserver* self,int psock):_this(self),_psock(psock){}tcpserver* _this;int _psock; };class tcpserver{ public:tcpserver(const uint16_t& port):_port(port),_listensock(-1){}void initserver(){//1.创建套接字_listensock=socket(AF_INET,SOCK_STREAM,0);if(_listensock<0){ logMessage(FATAL,"create listensocket error"); exit(SOCK_ERR);} logMessage(NORMAL, "create socket success: %d", _listensock);//2.bind ip和portstruct sockaddr_in local;local.sin_family=AF_INET;local.sin_port=htons(_port);local.sin_addr.s_addr=INADDR_ANY;if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//绑定失败{ logMessage(FATAL,"bind error"); exit(BIND_ERR);} logMessage(NORMAL,"bind success");//3.将套接字设置为监听模式if(listen(_listensock,0)<0){ logMessage(FATAL,"listen error"); exit(LISTEN_ERR);}logMessage(NORMAL,"listen success");}void start(){ // signal(SIGCHLD, SIG_IGN); threadPool<Task>::getthpptr()->run(); while(true) { struct sockaddr_in cli; socklen_t len=sizeof(cli); bzero(&cli,len); int sock=accept(_listensock,(struct sockaddr*)&cli,&len); if(sock<0) { logMessage(FATAL,"accept client error"); continue; } logMessage(NORMAL,"accept client success"); cout<<"accept sock: "<<sock<<endl; // serviceIO(sock);//客户端串行版 // close(sock); //多进程版--- //一个客户端占用一个文件描述符,原因在于孙子进程执行IO任务需要占用独立的文件描述符,而文件描述符是继承父进程的,而每次客户端进来都要占用新的文件描述符 //因此若接收多个客户端不退出的话文件描述符会越来越少。// pid_t id=fork();//创建子进程// if(id==0)//子进程进入// {// close(_listensock);//子进程不需要用于监听因此关闭该文件描述符// if(fork()>0) exit(0);// //子进程创建孙子进程,子进程直接退出,让孙子进程担任IO任务,且孙子进程成为孤儿进程被OS领养,// //除非客户端退出IO任务结束否则该孤儿进程一直运行下去不会相互干扰,即并行完成服务器和客户端的通信// //孙子进程// serviceIO(sock);// close(sock);// exit(0);// }// //父进程// pid_t ret=waitpid(id,nullptr,0);// if(ret<0)// {// cout << "waitsuccess: " << ret << endl;// }//多线程版// pthread_t pid;// ThreadData* th=new ThreadData(this,sock);// pthread_create(&pid,nullptr,start_routine,th);threadPool<Task>::getthpptr()->push(Task(sock,serviceIO)); }}// static void* start_routine(void* args)// {// pthread_detach(pthread_self());// ThreadData* ret=static_cast(args); // ret->_this->serviceIO(ret->_psock);// close(ret->_psock);// delete ret;// return nullptr;// } // void serviceIO(int sock)// {// char inbuffer[NUM];// while(true)// {// ssize_t n=read(sock,inbuffer,sizeof(inbuffer)-1);// if(n>0)// {// inbuffer[n]=0;// cout<<"recv message: "<// string outb=inbuffer;// string outbuffer=outb+"[server echo]";// write(sock,outbuffer.c_str(),outbuffer.size());// }// else// {// logMessage(NORMAL,"client quit,i quit yep");// break;// }// }// }~tcpserver(){}private:int _listensock;//用于监听服务器的sock文件描述符uint16_t _port;//端口号};}
1. 单进程版:客户端串行版
#pragma once#include #include #include #include #include #include #include #include #include #include #include #include #include"log.hpp"#define NUM 1024using namespace std;namespace Server{ enum { USAGE_ERR=1,SOCK_ERR,BIND_ERR,LISTEN_ERR };class tcpserver{ public:tcpserver(const uint16_t& port):_port(port),_listensock(-1){}void initserver(){//1.创建套接字_listensock=socket(AF_INET,SOCK_STREAM,0);if(_listensock<0){ logMessage(FATAL,"create listensocket error"); exit(SOCK_ERR);} logMessage(NORMAL, "create socket success: %d", _listensock);//2.bind ip和portstruct sockaddr_in local;local.sin_family=AF_INET;local.sin_port=htons(_port);local.sin_addr.s_addr=INADDR_ANY;if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//绑定失败{ logMessage(FATAL,"bind error"); exit(BIND_ERR);} logMessage(NORMAL,"bind success");//3.将套接字设置为监听模式if(listen(_listensock,0)<0){ logMessage(FATAL,"listen error"); exit(LISTEN_ERR);}logMessage(NORMAL,"listen success");}void start(){ while(true) { struct sockaddr_in cli; socklen_t len=sizeof(cli); bzero(&cli,len); int sock=accept(_listensock,(struct sockaddr*)&cli,&len); if(sock<0) { logMessage(FATAL,"accept client error"); continue; } logMessage(NORMAL,"accept client success"); cout<<"accept sock: "<<sock<<endl; serviceIO(sock);//客户端串行版 close(sock); }}void serviceIO(int sock){ char inbuffer[NUM]; while(true) { ssize_t n=read(sock,inbuffer,sizeof(inbuffer)-1); if(n>0) { inbuffer[n]=0; cout<<"recv message: "<<inbuffer<<endl; string outb=inbuffer; string outbuffer=outb+"[server echo]"; write(sock,outbuffer.c_str(),outbuffer.size()); }else{ logMessage(NORMAL,"client quit,i quit yep"); break;} }}~tcpserver(){}private:int _listensock;//用于监听服务器的sock文件描述符uint16_t _port;//端口号};}
注意:客户端串行給服务器发数据是在哪里堵塞?由于阻塞在accept函数处,即accept等待客户端接入是阻塞式等待。accept函数接收了一个连接请求后,后来的客户端连接请求需要在accept函数处等待,当上一个客户端退出后,服务器才能accept当前客户端发送来的连接请求成功,才能接收当前客户端的数据。即服务器串行接收处理客户端发送来的数据
2. 多进程版:客户端并行版
tcpserver.hpp
#pragma once#include #include #include #include #include #include #include #include #include #include #include #include #include"log.hpp"#define NUM 1024using namespace std;namespace Server{ enum { USAGE_ERR=1,SOCK_ERR,BIND_ERR,LISTEN_ERR };class tcpserver{ public:tcpserver(const uint16_t& port):_port(port),_listensock(-1){}void initserver(){//1.创建套接字_listensock=socket(AF_INET,SOCK_STREAM,0);if(_listensock<0){ logMessage(FATAL,"create listensocket error"); exit(SOCK_ERR);} logMessage(NORMAL, "create socket success: %d", _listensock);//2.bind ip和portstruct sockaddr_in local;local.sin_family=AF_INET;local.sin_port=htons(_port);local.sin_addr.s_addr=INADDR_ANY;if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//绑定失败{ logMessage(FATAL,"bind error"); exit(BIND_ERR);} logMessage(NORMAL,"bind success");//3.将套接字设置为监听模式if(listen(_listensock,0)<0){ logMessage(FATAL,"listen error"); exit(LISTEN_ERR);}logMessage(NORMAL,"listen success");}void start(){ // signal(SIGCHLD, SIG_IGN); while(true) { struct sockaddr_in cli; socklen_t len=sizeof(cli); bzero(&cli,len); int sock=accept(_listensock,(struct sockaddr*)&cli,&len); if(sock<0) { logMessage(FATAL,"accept client error"); continue; } logMessage(NORMAL,"accept client success"); cout<<"accept sock: "<<sock<<endl; //多进程版--- //一个客户端占用一个文件描述符,原因在于孙子进程执行IO任务需要占用独立的文件描述符,而文件描述符是继承父进程的,而每次客户端进来都要占用新的文件描述符 //因此若接收多个客户端不退出的话文件描述符会越来越少。 pid_t id=fork();//创建子进程 if(id==0)//子进程进入 { close(_listensock);//子进程不需要用于监听因此关闭该文件描述符 if(fork()>0) exit(0);// //子进程创建孙子进程,子进程直接退出,让孙子进程担任IO任务,且孙子进程成为孤儿进程被OS领养,// //除非客户端退出IO任务结束否则该孤儿进程一直运行下去不会相互干扰,即并行完成服务器和客户端的通信// //孙子进程serviceIO(sock);close(sock);exit(0); } //父进程 // close(sock);//父进程不使用文件描述符就关闭 pid_t ret=waitpid(id,nullptr,0); if(ret<0) { cout << "waitsuccess: " << ret << endl; } }}void serviceIO(int sock){ char inbuffer[NUM]; while(true) { ssize_t n=read(sock,inbuffer,sizeof(inbuffer)-1); if(n>0) { inbuffer[n]=0; cout<<"recv message: "<<inbuffer<<endl; string outb=inbuffer; string outbuffer=outb+"[server echo]"; write(sock,outbuffer.c_str(),outbuffer.size()); }else{ logMessage(NORMAL,"client quit,i quit yep"); break;} }}~tcpserver(){}private:int _listensock;//用于监听服务器的sock文件描述符uint16_t _port;//端口号};}
-
父进程fork创建子进程,创建完后waitpid等待回收子进程。子进程fork创建孙子进程,创建完后直接退出。导致孙子进程成为孤儿进程,进而被OS领养。因此除非客户端退出IO任务,否则孤儿进程将一直运行下去不会干扰到其他进程,即并行完成服务器和客户端的通信
-
注意的是服务器accept一次客户端的连接请求,就需要申请一个文件描述符,而文件描述符是有上限的,如果大量的客户端请求连接成功并且不结束的话,会造成文件描述符泄露。
因此在父进程那里需要关闭不使用的文件描述符
- 父进程这里回收子进程,不能使用非阻塞等待,原因在于非阻塞等待的本质是轮询,而这里使用后会导致父进程会在accept函数处阻塞等待客户端发送连接请求,那么父进程就无法回收子进程了。因此waitpid的返回值用ret接收,等待回收成功就打印日志,失败则跳过
- 当子进程图退出或者被中止时子进程会发送17号信号SIGCHILD给父进程,父进程可以通过忽略17号信号SIGCHILD的方式来不阻塞等待回收子进程(这种方法对于linux环境可用,其余不保证)
signal(SIGCHLD, SIG_IGN);
netstat查看网络信息
netstat
是一个用于查看网络连接和网络统计信息的命令行工具。它可以用来显示当前系统上的网络连接、路由表、接口统计信息等等。在 Linux 系统中,netstat
命令的用法如下:
netstat [options]
一些常用的选项包括:
-a
:显示所有的连接,包括监听中和已建立的连接。-t
:显示 TCP 协议的连接。-u
:显示 UDP 协议的连接。-n
:以数字形式显示 IP 地址和端口号,而不是尝试进行 DNS 解析。-p
:显示与连接关联的进程信息。-r
:显示路由表。-l
:仅显示监听中的连接。-atun
:显示所有的TCP和UDP连接
注意一下:这里出现了两个连接,原因在于服务器和客户端在同一台主机上,即服务器和客户端完成了本地环回,因此能看到两个连接。
3.多线程版:并行执行
tcpserver.hpp
#pragma once#include #include #include #include #include #include #include #include #include #include #include #include #include"log.hpp"#define NUM 1024using namespace std;namespace Server{ enum { USAGE_ERR=1,SOCK_ERR,BIND_ERR,LISTEN_ERR };class tcpserver; class ThreadData {public:ThreadData( tcpserver* self,int psock):_this(self),_psock(psock){}tcpserver* _this;int _psock; };class tcpserver{ public:tcpserver(const uint16_t& port):_port(port),_listensock(-1){}void initserver(){//1.创建套接字_listensock=socket(AF_INET,SOCK_STREAM,0);if(_listensock<0){ logMessage(FATAL,"create listensocket error"); exit(SOCK_ERR);} logMessage(NORMAL, "create socket success: %d", _listensock);//2.bind ip和portstruct sockaddr_in local;local.sin_family=AF_INET;local.sin_port=htons(_port);local.sin_addr.s_addr=INADDR_ANY;if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//绑定失败{ logMessage(FATAL,"bind error"); exit(BIND_ERR);} logMessage(NORMAL,"bind success");//3.将套接字设置为监听模式if(listen(_listensock,0)<0){ logMessage(FATAL,"listen error"); exit(LISTEN_ERR);}logMessage(NORMAL,"listen success");}void start(){ while(true) { struct sockaddr_in cli; socklen_t len=sizeof(cli); bzero(&cli,len); int sock=accept(_listensock,(struct sockaddr*)&cli,&len); if(sock<0) { logMessage(FATAL,"accept client error"); continue; } logMessage(NORMAL,"accept client success"); cout<<"accept sock: "<<sock<<endl; //多线程版pthread_t pid;ThreadData* th=new ThreadData(this,sock);pthread_create(&pid,nullptr,start_routine,th); }} static void* start_routine(void* args){ pthread_detach(pthread_self());//线程分离后让OS自动回收新线程 ThreadData* ret=static_cast<ThreadData*>(args); ret->_this->serviceIO(ret->_psock); close(ret->_psock); delete ret; return nullptr;} void serviceIO(int sock){ char inbuffer[NUM]; while(true) { ssize_t n=read(sock,inbuffer,sizeof(inbuffer)-1); if(n>0) { inbuffer[n]=0; cout<<"recv message: "<<inbuffer<<endl; string outb=inbuffer; string outbuffer=outb+"[server echo]"; write(sock,outbuffer.c_str(),outbuffer.size()); }else{ logMessage(NORMAL,"client quit,i quit yep"); break;} }}~tcpserver(){}private:int _listensock;//用于监听服务器的sock文件描述符uint16_t _port;//端口号};}
- 服务器接收一个客户端的连接请求,就申请一个新线程,多线程下可以让服务器接收多个线程
log.hpp
#pragma once#include #include #include #include #include #include #include using namespace std;#define DEBUG 0#define NORMAL 1#define WARNING 2#define ERROR 3#define FATAL 4#define NUM 1024#define LOG_STR "./logstr.txt"#define LOG_ERR "./log.err"const char* to_str(int level){ switch(level) { case DEBUG: return "DEBUG"; case NORMAL: return "NORMAL"; case WARNING: return "WARNING"; case ERROR: return "ERROR"; case FATAL: return "FATAL"; default: return nullptr; }}void logMessage(int level, const char* format,...){ // [日志等级] [时间戳/时间] [pid] [messge] // [WARNING] [2023-05-11 18:09:08] [123] [创建socket失败] // 暂定 // std::cout << message << std::endl;char logprestr[NUM];snprintf(logprestr,sizeof(logprestr),"[%s][%ld][%d]",to_str(level),(long int)time(nullptr),getpid());//把后面的内容打印进logprestr缓存区中char logeldstr[NUM];va_list arg;va_start(arg,format); vsnprintf(logeldstr,sizeof(logeldstr),format,arg);//arg是logmessage函数列表中的... cout<<logprestr<<logeldstr<<endl;// FILE* str=fopen(LOG_STR,"a");// FILE* err=fopen(LOG_ERR,"a");//以追加方式打开文件,若文件不存在则创建 // if(str!=nullptr||err!=nullptr)//两个文件指针都不为空则创建文件成功// {// FILE* ptr=nullptr;// if(level==DEBUG||level==NORMAL||level==WARNING)// {// ptr=str;// }// if(level==ERROR||level==FATAL)// {// ptr=err;// }// if(ptr!=nullptr)// {// fprintf(ptr,"%s-%s\n",logprestr,logeldstr);// }// fclose(str);// fclose(err); //}}
可变参数列表
va_list是(char*)重命名的类型,定义可以访问可变参数的变量。
va_start(ap, v) ap是定义的可变参数变量,v是形参中可变参数前第一个参数名,其作用是使ap指向可变参数部分。
va_arg(ap, t) ap是定义的可变参数变量,t是可变参数的类型,根据类型,访问可变参数列表中的数据。
va_end(ap) ap是定义的可变参数变量,使ap变量置空,作为结束使用。
vsnprintf函数原型
#include int vsnprintf(char *str, size_t size, const char *format, va_list ap);
-
str是一个指向字符数组(缓冲区)的指针,用于存储格式化后的数据
-
size是缓冲区的大小,限制了写入的最大字符数,包括终止的 null 字符
-
format格式化字符串,类似于
printf
函数中的格式化字符串 -
ap是一个
va_list
类型的变量,用于存储可变参数列表的信息,并且要注意OS对参数压栈的顺序是从右向左 -
vsnprintf
函数根据指定的format
格式化字符串将数据写入str
缓冲区,但不会超出指定的缓冲区大小。它会在写入数据后自动在缓冲区末尾添加一个 null 终止字符,确保结果是一个合法的 C 字符串。
守护进程
守护进程(Daemon)是在计算机系统中以后台方式运行的一类特殊进程。它通常在操作系统启动时被初始化,并在整个系统运行期间保持活动状态,不需要与用户交互。守护进程通常用于执行系统任务、服务管理以及提供后台服务,如网络服务、定时任务等。
守护进程特点如下:
- 后台运行,守护进程在后台运行,不与用户交互,没有控制终端。
- 独立性:它通常独立于用户会话,即使用户注销或关闭终端,守护进程也会继续运行。
- 没有标准输入输出:守护进程通常没有标准输入和输出,因为它们不与用户交互。它们通常将输出写入日志文件。
- 分离自身:守护进程会通过一系列操作来与终端、会话和控制组脱离连接,以确保它不会意外地被控制终端关闭。
一个服务器中可以具有多个会话,例如一个服务器上有一个root用户和多个普通用户,当普通用户登录上服务器时即成为一个会话。
一个会话具有多个后台任务,但只能具有一个前台任务(bash)。
- jobs查看任务可以看到任务1是./tcpserver,任务2是sleep 1000 | sleep 2000 | sleep 3000 &,任务3是sleep 4000 | sleep 5000 &,且三个任务后面都带&,在进程或任务后带&作用是将该任务放到后台运行
- sleep 1000 、sleep 2000 、sleep 3000 、sleep 4000、sleep 5000的父进程都是16853即bash;而 sleep 1000 、sleep 2000 、sleep 3000的PGID相同,都是sleep 1000的pid,即 sleep 1000 、sleep 2000 、sleep 3000属于同一组,同一个组要协同起来完成同一个作业。第一个任务的pid是组长的pid即sleep 1000的pid;而小组16858和小组17070的SID都是16853,即这两个小组属于同一个会话(bash),要完成的是同一个任务;
fg、bg
fg 作业号:将作业放到前台
bg 作业号:将作业放到后台,或者继续执行后台作业
ctrl+Z将前台任务暂停并把作业放到后台
- 用户登录时服务器就需要为此创建一些后台作业和前台作业(命令行)来服务用户,而用户注销或退出服务器也会影响其前台作业和后台作业。而服务器程序不能受到用户登录和注销的影响。
- 我们可以使得服务器程序自成会话,自成进程组,那么该程序就与终端设备无关,不能再收到用户登录和注销的影响了。该类进程被称为守护进程
setsid
在Unix和类Unix系统中,
setsid
是一个用于创建新会话的系统调用函数。会话(Session)是一组相关的进程组合,通常由一个控制终端和一些子进程组成。setsid
函数的主要作用是将调用它的进程从当前会话中分离出来,并创建一个新的会话。
#include pid_t setsid(void);
- 创建新会话:调用
setsid
的进程会成为一个新的会话的组长(Session Leader)。新会话不再与之前的控制终端相关联。但该进程在调用setsid函数之前不能是组长。 - 分离终端:调用
setsid
的进程不再与任何控制终端关联,无法重新获得控制终端。 - 成为新进程组的组长:新会话中的第一个进程(调用
setsid
的进程)会成为新的进程组的组长。
daemon.hpp
#pragma once#include #include #include #include #include #include #include #define DEV "/dev/null"void daemonSelf(const char *currPath = nullptr){ // 1. 让调用进程忽略掉异常的信号signal(SIGPIPE,SIG_IGN);//选择忽略SIGPIPE信号 // 2. 如何让自己不是组长,setsidif(fork()>0)exit(0);//父进程退出 // 子进程 -- 守护进程,精灵进程,本质就是孤儿进程的一种!pid_t ret=setsid();assert(ret!=-1); // 3. 守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件int fd=open(DEV,O_RDWR);if(fd>=0){ //dup2(oldfd,newfd):将oldfd的内容填充到newfd中,这样输入到newfd的内容被重定向到oldfd dup2(fd,0); dup2(fd,1); dup2(fd,2);}else{ close(0); close(1); close(2);} // 4. 可选:进程执行路径发生更改if(currPath) chdir(currPath);//更改currPath的路径}
/dev/null
是一个特殊的设备文件,它被用作数据丢弃点,向它写入的数据会被丢弃,从它读取数据会立即返回EOF(End of File)- SIGPIPE的触发场景:当一个进程向一个已经关闭写端的管道(或者套接字)写数据时、当进程向一个已经收到
RST
包(连接重置)的套接字发送数据时,该进程就会向父进程发送SIGPIPE信号来进行进程终止。对SIGPIPE进行忽略行为避免了进程向/dev/null中写入数据并出现错误导致的进程终止 - 父进程创建子进程,父进程作为组长,父进程退出后,子进程能够自己成为组长即能够成为守护进程
- dup2(oldfd,newfd):将oldfd的内容填充到newfd中,这样输入到newfd的内容被重定向到oldfd。在代码中是将输入文件描述符012的内容重定向到fd即/dev/null中
tcpserver.cc
#include"tcpserver.hpp"#include"log.hpp"#include"daemon.hpp"#include #include #include using namespace Server;using namespace std;static void Usage(string proc){ cout<<"\nUsage:\n\t"<<proc<<" local_port\n\n"<<endl;}int main(int argc,char* argv[]){if(argc!=2){ Usage(argv[0]); exit(USAGE_ERR);}uint16_t port=atoi(argv[1]);//将字符串转化为整数unique_ptr<tcpserver> ts(new tcpserver(port));ts->initserver();daemonSelf();ts->start();return 0;}
TCP协议通信流程
首先需要服务器初始化
服务器初始化:
- 调用socket, 创建文件描述符,该文件描述符用于监听;
- 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
- 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 该文件描述符处于监听状态,等待客户端发起连接;
- 调用accecpt, 并阻塞, 等待客户端连接过来;
建立连接的过程(通常称为三次握手)
建立连接的过程(内含三次握手)
- 客户端调用socket,创建文件描述符;
- 客户端调用connect,向指定地址端口的服务器发起请求;(请求的过程中会进行三次握手)
- connect会发出SYN段給服务器并阻塞等待服务器应答;(第一次握手)
- 服务器收到客户端的SYN段后,会应答一个SYN-ACK段表示"同意建立连接";(第二次握手)
- 客户端收到SYN-ACK后,会从connet()返回,同时发送一个应答ACK段給服务器;(第三次握手)
- 服务器收到客户端发来的ACK段后,会从accpet()返回,返回(分配)一个新的文件描述符connfd用于与客户端通信
- 可以看到三次握手由connect()发起请求开始,并由connect()返回结束,因此客户端在调用connect()时本质就是通过某种方式与服务器进行三次握手
**对于建链接的3次握手,**主要是要初始化Sequence Number 的初始值。通信的双方要互相通知对方自己的初始化的Sequence Number(缩写为ISN:Inital Sequence Number)——所以叫SYN,全称Synchronize Sequence Numbers。也就上图中的 x 和 y。这个号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输的问题而乱序(TCP会用这个序号来拼接数据)。来自陈浩大佬对于三次握手的部分诠释
- 连接建立成功后会被accpet获取到,此时客户端和服务器就能进行通信了。要注意的是,连接建立是三次握手做的事,三次握手是TCP底层的工作,而accep要做的是把底层已经建立好的连接拿到用户层,即accept本身不参于三次握手这个过程(不参与建立连接),accpet会阻塞等待获取建立好的连接,若连接没有建立好会进行等待。
数据传输的过程
建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方 可以同时写数据; 其原因在于服务器和客户端的应用层和传输层都有两个缓冲区,一个是发送缓冲区另一个是接收缓冲区,那么服务器和客户端进行发送和读取并不会互相影响。相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;
服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;
这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期 间客户端调用read()阻塞等待服务器的应答;
服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;
客户端收到后从read()返回, 发送下一条请求,如此循环下去
断开连接的过程
- 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段;(第一次握手)
- 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 ;(第二次握手)
- read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送 一个FIN; (第三次握手)
- 客户端收到FIN, 再返回一个ACK给服务器; (第四次握手)
-
这个断开连接的过程, 通常称为四次挥手
-
对于4次挥手其实你仔细看是2次,因为TCP是全双工的,所以,发送方和接收方都需要Fin和Ack。只不过,有一方是被动的,所以看上去就成了所谓的4次挥手。如果两边同时断连接,那就会就进入到CLOSING状态,然后到达TIME_WAIT状态。
当客户端不与服务器通信时需要断开连接的原因
- 其实,网络上的传输是没有连接的,包括TCP也是一样的。而TCP所谓的“连接”,其实只不过是在通讯的双方维护一个“连接状态”,让它看上去好像有连接一样。所以,TCP的状态变换是非常重要的。若通信结束不及时断开连接,即占用着操作系统的资源不使用,会导致系统的资源越来越少。
- 服务器能够与多个客户端建立连接,意味着服务器会收到大量的连接,因此操作系统要对这些连接进行管理,即"先组织再管理",在服务端就需要维护连接相关的数据结构,把这些数据结构组织起来,那么对连接的管理转变为对数据结构的管理。
- 操作系统需要维护这些连接相关的数据结构,势必需要消耗资源,而不通信的连接不断开,会导致操作系统的资源浪费。而TCP与UDP的区别之一在于TCP需要对连接相关的资源进行管理。
来源地址:https://blog.csdn.net/m0_71841506/article/details/132509902