Linux:(socket套接字——TCP协议,守护进程)
目录
一、TCP协议相关函数
1.socket、bind函数
2.listen函数
3.accept函数
4.connect函数
二、实现TCP通信
1.服务端实现
(1)服务端类
(2)初始化服务端
(3)启动服务器
(4)hander任务函数
(5)析构函数
(6)main函数
(7)总代码
2.客户端实现
(1)客户端类
(2)初始化客户端
(3)启动客户端
(4)处理任务函数
(5)main函数
(6)总代码
三、代码改造
1.多进程版本
2.多线程版本
3.线程池版本
四、线程池版——最终代码
1.线程池等准备类
2.服务端
3.客户端
五、守护进程
1.什么是守护进程
2.前后台进程组
3.将进程变为守护进程
4.关于daemon的说明
一、TCP协议相关函数
TCP与UDP协议使用的套接字接口比较相似,但TCP需要使用的接口更多,细节也会更多。
1.socket、bind函数
这两个函数在该篇博文第二部分socket套接字的第一部分有介绍。
2.listen函数
int listen(int sockfd, int backlog);
-
头文件:sys/socket.h
-
功能:设置该文件描述符为监听状态。
-
参数:int sockfd表示之前使用socket()返回的文件描述符sockfd,int backlog这个参数以后再说,现在说不明白。
-
返回值:成功返回另一个用于通信的文件描述符,失败返回-1并设置错误码errno。
3.accept函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-
头文件:sys/socket.h
-
功能:将网络中发来的连接申请进行连接。
-
参数:int sockfd表示之前使用listen()返回的文件描述符。
struct sockaddr *addr是一个输出型参数,可以把与该进程连接的进程的网络信息填入其中。
socklen_t *addrlen是struct sockaddr的大小。
-
返回值:成功返回0,失败返回-1并设置错误码errno。
4.connect函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
头文件:sys/socket.h、sys/types.h
-
功能:将网络中发来的连接申请进行连接。
-
参数:int sockfd表示之前使用listen()返回的文件描述符。
struct sockaddr *addr是一个输出型参数,可以把与该进程连接的进程的网络信息填入其中。
socklen_t *addrlen是struct sockaddr的大小。
-
返回值:成功返回0,失败返回-1并设置错误码errno。
二、实现TCP通信
1.服务端实现
(1)服务端类
服务端类的构建与前面基本一致,全部构建在server.hpp。但成员变量只包括一个文件描述符_listensock,还有一个端口号_port,可以设为缺省值。
class Server
{
public:// 构造函数Server(uint16_t port): _port(port), _ip(ip), _listensocket(-1){}// 初始化void ServerInit(){}//处理方式void hander(int sock){}//启动服务端进程void ServerStart(){}//析构函数~Server(){}private:uint16_t _port; // 端口号string _ip; // ip地址int _listensocket; // socket()返回值
};
(2)初始化服务端
ServerInit()用于初始化服务端,由于TCP属于可靠传输,它对于网络连接的可靠性比较苛刻,所以它的服务端初始化步骤也会多一些。二者的服务端流程如下:
UDP:socket函数获取文件描述符fd->bind函数绑定IP
TCP:socket获取一个文件描述符fd1->bind绑定IP->用listen函数设置fd1为监听状态
我们的函数就是按照桑面TCP描述的顺序socket,bind,listen,当函数发生问题时打印错误信息并以对应错误码退出即可。
注意TCP协议是面向字节流的,socket的选项改为SOCK_STREAM。
// 初始化void ServerInit(){//_listeansocket是用来监听,不是用来通信的端口号_listensocket = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字cout << "socket success" << endl;struct sockaddr_in peer;peer.sin_family = AF_INET; // 表示使用ipv4peer.sin_port = htons(_port); // 将端口号网络字节序peer.sin_addr.s_addr = inet_addr(_ip.c_str()); // 传入IP地址int n = bind(_listensocket, (const struct sockaddr *)&peer, sizeof(peer)); // 绑定1pif (n < 0){cout << "bind flase" << endl;exit(1);}cout << "bind success" << endl;// listen函数使用主动连接套接字变为被连接套接口// ,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。// 在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。n = listen(_listensocket, backlog);if (n < 0){cout << "listen false" << endl;exit(2);}cout << "listen success" << endl;}
(3)启动服务器
启动服务器需要使用accept函数等待客户端的连接,无连接请求会阻塞等待,出现连接请求则会返回一个用于通信的文件描述符 。
然后就是处理客户端任务了,注意处理完成后要归还通信的文件描述符。
//启动服务端进程void ServerStart(){struct sockaddr_in addr; // 存储客户端的信息socklen_t len = sizeof(addr);// 如果没有客户端连接服务端,则accept会阻塞等待新连接// 如果有客户端需要连接服务端,则返回一个用于网络通信的描述符sockint sock = accept(_listensocket, (struct sockaddr *)&addr, &len);if (sock < 0){cout << "accept false" << endl;exit(3);}cout << "accept success" << endl;//不断的处理客户端发来的任务hander(sock);//退出该函数时,客户端已经退出,需要将进行网络传输的文件描述符释放,否则会引起资源泄露close(sock);//服务端释放通信的文件描述符,但不释放监听的文件描述符//服务端进程可以继续使用该监听文件描述符阻塞等待下一个客户端连接}
(4)hander任务函数
这个函数负责将客户端发来的数据打印到自己的屏幕上,然后再发回客户端。
由于网络接口集成在了文件接口中,所以使用read和write进行收发信息。
//处理方式void hander(int sock){char buffer[SIZE]; // 定义缓冲区,用于存放客户端发来的数据while (true){size_t n = read(sock, buffer, sizeof(buffer) - 1); // 减一是为了留出一个\0if (n > 0){buffer[n] = '\0'; // 把发来的\n覆盖掉cout << "Server get a data: " << buffer << endl;string out;out += buffer;write(sock, out.c_str(), out.size()); //将数据写回}else{cout<<"Client quit"<<endl;break;}}}
(5)析构函数
按道理监听的文件描述符也应当在不用时释放,但我们选择不释放。
//析构函数//按道理不释放监听文件描述符也是一种资源泄漏,但是服务器进程大部分需要长期运行//只有进程出问题了进程才会退出,而当进程退出时,它占用的资源也归还了操作系统,所以不释放也无所谓~Server(){}
(6)main函数
#include<memory>
#include"server.hpp"using namespace std;int main(int argc,char* argv[])
{uint16_t port=atoi(argv[1]);unique_ptr<Server> p(new Server(port));p->ServerInit();p->ServerStart();return 0;
}
(7)总代码
server.hpp
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdlib.h>using namespace std;
string ip = "0.0.0.0";
int backlog = 5;
#define SIZE 1024class Server
{
public:// 构造函数Server(uint16_t port): _port(port), _ip(ip), _listensocket(-1){}// 初始化void ServerInit(){//_listeansocket是用来监听,不是用来通信的端口号_listensocket = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字cout << "socket success" << endl;struct sockaddr_in peer;peer.sin_family = AF_INET; // 表示使用ipv4peer.sin_port = htons(_port); // 将端口号网络字节序peer.sin_addr.s_addr = inet_addr(_ip.c_str()); // 传入IP地址int n = bind(_listensocket, (const struct sockaddr *)&peer, sizeof(peer)); // 绑定1pif (n < 0){cout << "bind flase" << endl;exit(1);}cout << "bind success" << endl;// listen函数使用主动连接套接字变为被连接套接口// ,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。// 在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。n = listen(_listensocket, backlog);if (n < 0){cout << "listen false" << endl;exit(2);}cout << "listen success" << endl;}//处理方式void hander(int sock){char buffer[SIZE]; // 定义缓冲区,用于存放客户端发来的数据while (true){size_t n = read(sock, buffer, sizeof(buffer) - 1); // 减一是为了留出一个\0if (n > 0){buffer[n] = '\0'; // 把发来的\n覆盖掉cout << "Server get a data: " << buffer << endl;string out;out += buffer;write(sock, out.c_str(), out.size()); //将数据写回}else{cout<<"Client quit"<<endl;break;}}}//启动服务端进程void ServerStart(){struct sockaddr_in addr; // 存储客户端的信息socklen_t len = sizeof(addr);// 如果没有客户端连接服务端,则accept会阻塞等待新连接// 如果有客户端需要连接服务端,则返回一个用于网络通信的描述符sockint sock = accept(_listensocket, (struct sockaddr *)&addr, &len);if (sock < 0){cout << "accept false" << endl;exit(3);}cout << "accept success" << endl;//不断的处理客户端发来的任务hander(sock);//退出该函数时,客户端已经退出,需要将进行网络传输的文件描述符释放,否则会引起资源泄露close(sock);//服务端释放通信的文件描述符,但不释放监听的文件描述符//服务端进程可以继续使用该监听文件描述符阻塞等待下一个客户端连接}//析构函数//按道理不释放监听文件描述符也是一种资源泄漏,但是服务器进程大部分需要长期运行//只有进程出问题了进程才会退出,而当进程退出时,它占用的资源也归还了操作系统,所以不释放也无所谓~Server(){}private:uint16_t _port; // 端口号string _ip; // ip地址int _listensocket; // socket()返回值
};
servermain.cc
#include<memory>
#include"server.hpp"using namespace std;int main(int argc,char* argv[])
{uint16_t port=atoi(argv[1]);unique_ptr<Server> p(new Server(port));p->ServerInit();p->ServerStart();return 0;
}
2.客户端实现
(1)客户端类
客户端类的构建与前面基本一致
class Client
{
public://构造函数Client(string& ip,uint16_t& port):_ip(ip),_port(port),_socket(-1){}//初始化void ClientInit(){}void ClientStart(){}//析构函数要释放不使用的文件描述符~Client(){close(_socket);}
private:int _socket;//套接字string _ip;//ip地址uint16_t _port;//端口号
};
(2)初始化客户端
ClientInit()用于初始化客户端,只需要创建套接字就可以了。
//初始化void ClientInit(){_socket=socket(AF_INET, SOCK_STREAM, 0);//创建套接字if(_socket<0){cout<<"socket false"<<endl;exit(1);}}
(3)启动客户端
客户端需要使用connect函数向服务端发连接请求,连接成功则循环发送并接收数据。
//客户端启动void ClientStart(){struct sockaddr_in local;local.sin_family=AF_INET; // 表示使用ipv4local.sin_port=htons(_port); // 将端口号网络字节序local.sin_addr.s_addr=inet_addr(_ip.c_str()); // 传入IP地址socklen_t len=sizeof(local);//客户端连接服务器int n=connect(_socket,(struct sockaddr*)&local,len);if(n<0){cout<<"connect false"<<endl;exit(2);}string msg;while(true){//向服务器发送数据cout<<"Client say#";getline(cin,msg);write(_socket,msg.c_str(),msg.size());//接收服务器返回的数据char buffer[1024];int m=read(_socket,buffer,sizeof(buffer)-1);if(m>0){buffer[m]='\0';cout<<"Server return: "<<buffer<<endl;}else{//read返回0,证明服务端关闭,客户端也要关闭cout<<"Server quit"<<endl;break;} }}
(4)处理任务函数
这个函数负责将客户端发来的数据打印到自己的屏幕上,然后再发回客户端。
由于网络接口集成在了文件接口中,所以使用read和write进行收发信息。
//客户端启动void ClientStart(){struct sockaddr_in local;local.sin_family=AF_INET; // 表示使用ipv4local.sin_port=htons(_port); // 将端口号网络字节序local.sin_addr.s_addr=inet_addr(_ip.c_str()); // 传入IP地址socklen_t len=sizeof(local);//客户端连接服务器int n=connect(_socket,(struct sockaddr*)&local,len);if(n<0){cout<<"connect false"<<endl;exit(2);}string msg;while(true){//向服务器发送数据cout<<"Client say#";getline(cin,msg);write(_socket,msg.c_str(),msg.size());//接收服务器返回的数据char buffer[1024];int m=read(_socket,buffer,sizeof(buffer)-1);if(m>0){buffer[m]='\0';cout<<"Server return: "<<buffer<<endl;}else{//read返回0,证明服务端关闭,客户端也要关闭cout<<"Server quit"<<endl;break;} }}
(5)main函数
main函数也可以直接使用以前的main函数实现。
#include"client.hpp"
#include<memory>using namespace std;int main(int args,char* argv[])
{uint16_t port=atoi(argv[2]);string ip=argv[1];unique_ptr<Client> p(new Client(ip,port));p->ClientInit();p->ClientStart();return 0;
}
(6)总代码
client.hpp
#include<iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdlib.h>using namespace std;class Client
{
public://构造函数Client(string& ip,uint16_t& port):_ip(ip),_port(port),_socket(-1){}//初始化void ClientInit(){_socket=socket(AF_INET, SOCK_STREAM, 0);//创建套接字if(_socket<0){cout<<"socket false"<<endl;exit(1);}}//客户端启动void ClientStart(){struct sockaddr_in local;local.sin_family=AF_INET; // 表示使用ipv4local.sin_port=htons(_port); // 将端口号网络字节序local.sin_addr.s_addr=inet_addr(_ip.c_str()); // 传入IP地址socklen_t len=sizeof(local);//客户端连接服务器int n=connect(_socket,(struct sockaddr*)&local,len);if(n<0){cout<<"connect false"<<endl;exit(2);}string msg;while(true){//向服务器发送数据cout<<"Client say#";getline(cin,msg);write(_socket,msg.c_str(),msg.size());//接收服务器返回的数据char buffer[1024];int m=read(_socket,buffer,sizeof(buffer)-1);if(m>0){buffer[m]='\0';cout<<"Server return: "<<buffer<<endl;}else{//read返回0,证明服务端关闭,客户端也要关闭cout<<"Server quit"<<endl;break;} }}//析构函数要释放不使用的文件描述符~Client(){close(_socket);}
private:int _socket;//套接字string _ip;//ip地址uint16_t _port;//端口号
};
clientmain.cc
#include"client.hpp"
#include<memory>using namespace std;int main(int args,char* argv[])
{uint16_t port=atoi(argv[2]);string ip=argv[1];unique_ptr<Client> p(new Client(ip,port));p->ClientInit();p->ClientStart();return 0;
}
三、代码改造
1.多进程版本
我们使用创建多进程的方式处理hander任务,只需要将start函数改变即可,实现的原理可以看注释。
// 启动服务端进程——多进程版本void ServerStart1(){struct sockaddr_in addr; // 存储客户端的信息socklen_t len = sizeof(addr);// 如果没有客户端连接服务端,则accept会阻塞等待新连接// 如果有客户端需要连接服务端,则返回一个用于网络通信的描述符sockint sock = accept(_listensocket, (struct sockaddr *)&addr, &len);if (sock < 0){cout << "accept false" << endl;exit(3);}cout << "accept success" << endl;pid_t id = fork();if (id == 0){// 由于子进程也继承了父进程的监听套接字,而监听套接字只需要一个,所以需要关闭close(_listensocket);if (fork() > 0){// 子进程再次创建子进程,fork返回孙子进程的pidexit(0); // 服务器的子进程退出,使孙子进程变成了孤儿进程}// 不断的处理客户端发来的任务hander(sock);// 退出该函数时,客户端已经退出,需要将进行网络传输的文件描述符释放,否则会引起资源泄露close(sock);//退出孙子进程,由于他是孤儿进程,所以会被操作系统自行回收exit(0);}}
2.多线程版本
我们使用创建多线程的方式处理客户端任务,也只需要将start函数改变即可,实现的原理可以看注释。
class ThreadData
{
public:ThreadData(int fd, Server *t) : sockfd(fd), tsvr(t){}public:int sockfd;Server *tsvr;
};
static void *Routine(void *args){pthread_detach(pthread_self());//将线程分离,执行完毕后操作系统自动回收ThreadData *td = static_cast<ThreadData *>(args);td->tsvr->hander(td->sockfd);delete td;return nullptr;}// 启动服务端进程——多线程版本void ServerStart3(){struct sockaddr_in addr; // 存储客户端的信息socklen_t len = sizeof(addr);// 如果没有客户端连接服务端,则accept会阻塞等待新连接// 如果有客户端需要连接服务端,则返回一个用于网络通信的描述符sockint sock = accept(_listensocket, (struct sockaddr *)&addr, &len);if (sock < 0){cout << "accept false" << endl;exit(3);}cout << "accept success" << endl;// 创建新线程pthread_t tid;ThreadData *p = new ThreadData(sock, this);pthread_create(&tid, nullptr, Routine, p);//创造线程并且执行sleep(20);//这里睡眠20s,防止线程结束太快,来不及调用其他函数和打印}
3.线程池版本
// 启动服务端进程——线程池void ServerStart4(){Threadpool<ThreadData> td(10);td.Create();struct sockaddr_in addr; // 存储客户端的信息socklen_t len = sizeof(addr);// 如果没有客户端连接服务端,则accept会阻塞等待新连接// 如果有客户端需要连接服务端,则返回一个用于网络通信的描述符sockint sock = accept(_listensocket, (struct sockaddr *)&addr, &len);if (sock < 0){cout << "accept false" << endl;exit(3);}cout << "accept success" << endl;ThreadData t(sock);td.push(t);}
四、线程池版——最终代码
1.线程池等准备类
Task.hpp:
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdlib.h>
#include<functional>using namespace std;class ThreadData
{
public:ThreadData(int fd) : sockfd(fd){}void run(){char buffer[1024]; // 定义缓冲区,用于存放客户端发来的数据while (true){size_t n = read(sockfd, buffer, sizeof(buffer) - 1); // 减一是为了留出一个\0if (n > 0){buffer[n] = '\0'; // 把发来的\n覆盖掉cout << "Server get a data: " << buffer << endl;string out;out += buffer;write(sockfd, out.c_str(), out.size()); // 将数据写回}else{cout << "Client quit" << endl;break;}}}public:int sockfd;
};
threadpool.hpp:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <queue>using namespace std;class threadname
{
public:string _name; //线程的名字pthread_t tid; //线程的tid
};template <class T>
class Threadpool
{
private://加锁void Lock(){pthread_mutex_lock(&_mutex);}//解锁void Unlock(){pthread_mutex_unlock(&_mutex);}//线程等待void Wait(){pthread_cond_wait(&_cond, &_mutex);}//唤醒线程void signal(){pthread_cond_signal(&_cond);}//输出线程的名字string Getthreadname(pthread_t tid){for (auto ti : _v){if (ti.tid == tid){return ti._name;}}return nullptr;}public://构造函数Threadpool(int max = 10): _max(max), _v(max) //初始化数组和_max{pthread_mutex_init(&_mutex, nullptr); //初始化锁pthread_cond_init(&_cond, nullptr); //初始化条件变量}static void *hander(void *args){ Threadpool<T> *td = static_cast<Threadpool<T> *>(args);string name = td->Getthreadname(pthread_self()); //通过tid,将该线程的名字从数组中拿出while (true){td->Lock();while (td->_q.empty()) //队列如果为空,则进行线程等待{td->Wait();}T t = td->pop(); //将任务从队列中拿出td->Unlock();t.run();//cout << name << " a result:" << t.GetResult() << endl;}}//创造线程void Create(){int num = _v.size();for (int i = 0; i < num; i++){_v[i]._name = "thread_" + to_string(i + 1); //将线程的名字存入数组中pthread_create(&(_v[i].tid), nullptr, hander, (void *)this);cout<<_v[i]._name<<endl; //打印创造的线程名字}}//将任务添加到队列中void push(T &data){Lock();_q.push(data);//cout << "thread produser a task:" << data.GetTask() << endl;signal();Unlock();}//从队列中拿出任务T pop(){T t = _q.front();_q.pop();return t;}//析构函数~Threadpool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);for(int i=0;i<_max;i++){pthread_join(_v[i].tid,nullptr);}}private:vector<threadname> _v; //定义一个数组,用来存放线程的名字和tidqueue<T> _q; //队列,用来存放任务int _max; // 数组的大小pthread_mutex_t _mutex; // 锁pthread_cond_t _cond;
};
2.服务端
servermain.cc:
#include<memory>
#include"server.hpp"using namespace std;int main(int argc,char* argv[])
{uint16_t port=atoi(argv[1]);unique_ptr<Server> p(new Server(port));p->ServerInit();p->ServerStart4();return 0;
}
server.hpp:
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdlib.h>
#include "threadpool.hpp"
#include <functional>
#include "Task.hpp"using namespace std;
string ip = "0.0.0.0";
int backlog = 5;
#define SIZE 1024//线程池版本
class Server
{
public:// 构造函数Server(uint16_t port): _port(port), _ip(ip), _listensocket(-1){}// 初始化void ServerInit(){//_listeansocket是用来监听,不是用来通信的端口号_listensocket = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字cout << "socket success" << endl;struct sockaddr_in peer;peer.sin_family = AF_INET; // 表示使用ipv4peer.sin_port = htons(_port); // 将端口号网络字节序peer.sin_addr.s_addr = inet_addr(_ip.c_str()); // 传入IP地址int n = bind(_listensocket, (const struct sockaddr *)&peer, sizeof(peer)); // 绑定1pif (n < 0){cout << "bind flase" << endl;exit(1);}cout << "bind success" << endl;// listen函数使用主动连接套接字变为被连接套接口// ,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。// 在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。n = listen(_listensocket, backlog);if (n < 0){cout << "listen false" << endl;exit(2);}cout << "listen success" << endl;}~Server(){}// 启动服务端进程——线程池void ServerStart4(){Threadpool<ThreadData> td(10);td.Create();struct sockaddr_in addr; // 存储客户端的信息socklen_t len = sizeof(addr);// 如果没有客户端连接服务端,则accept会阻塞等待新连接// 如果有客户端需要连接服务端,则返回一个用于网络通信的描述符sockint sock = accept(_listensocket, (struct sockaddr *)&addr, &len);if (sock < 0){cout << "accept false" << endl;exit(3);}cout << "accept success" << endl;ThreadData t(sock);td.push(t);}private:uint16_t _port; // 端口号string _ip; // ip地址int _listensocket; // socket()返回值
};
3.客户端
clientmain.cc:
#include"client.hpp"
#include<memory>using namespace std;int main(int args,char* argv[])
{uint16_t port=atoi(argv[2]);string ip=argv[1];unique_ptr<Client> p(new Client(ip,port));p->ClientInit();p->ClientStart();return 0;
}
client.hpp:
#include<iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdlib.h>using namespace std;class Client
{
public://构造函数Client(string& ip,uint16_t& port):_ip(ip),_port(port),_socket(-1){}//初始化void ClientInit(){_socket=socket(AF_INET, SOCK_STREAM, 0);//创建套接字if(_socket<0){cout<<"socket false"<<endl;exit(1);}}//客户端启动void ClientStart(){struct sockaddr_in local;local.sin_family=AF_INET; // 表示使用ipv4local.sin_port=htons(_port); // 将端口号网络字节序local.sin_addr.s_addr=inet_addr(_ip.c_str()); // 传入IP地址socklen_t len=sizeof(local);//客户端连接服务器int n=connect(_socket,(struct sockaddr*)&local,len);if(n<0){cout<<"connect false"<<endl;exit(2);}cout<<"connect: "<<n<<endl;string msg;while(true){//向服务器发送数据cout<<"Client say#";getline(cin,msg);write(_socket,msg.c_str(),msg.size());//接收服务器返回的数据char buffer[1024];int m=read(_socket,buffer,sizeof(buffer)-1);if(m>0){buffer[m]='\0';cout<<"Server return: "<<buffer<<endl;}else{//read返回0,证明服务端关闭,客户端也要关闭cout<<"Server quit"<<endl;break;} }}//析构函数要释放不使用的文件描述符~Client(){close(_socket);}
private:int _socket;//套接字string _ip;//ip地址uint16_t _port;//端口号
};
运行结果:
五、守护进程
1.什么是守护进程
我们让服务器进程运行起来
打开另一个会话,使用netstat -lntp查看使用网络的进程网络信息
其中,第一个协议为tcp,IP第hi为0.0.0.0,端口号为8080的s进程就是我们的服务器进程。
此时我们关闭运行服务器进程的Xshell对话框,再从另一个没关闭的对话框内再次查看网络进程信息,此时发现我们的服务器进程不见了。
那问题就出现了,我只是关闭会话,并没让服务器进程退出,怎么服务器进程也没了。
实际上,我们每次在Xshell上创建一个会话,Linux机器上都会运行一个名字为bash的进程。每一个会话中允许拥有一个前台任务和多个后台任务。
当Xshell的窗口关闭后,Linux机器上对应的会话结束,bash进程退出,bash维护的所有进程也会退出。所以关掉Xshell窗口后该窗口进程也会退出。
而服务器往往是需要长期运行的,我们总不能老开着Xshell吧。所以就需要让服务器进程成为另一个独立会话管理的进程,这样进程就不会被其他的会话打扰了。
这种进程叫做守护进程,也叫做精灵进程。
2.前后台进程组
上图中,sleep 10000 | sleep 20000 | sleep 30000 &是通过管道一起创建的3个后台进程,这三个进程组成一个进程组,每个进程组也被叫做一个作业(你可以理解为高空作业的那个作业,不是写的作业)。后面的&表示这个作业是后台进程。
使用指令jobs可以查看当前机器上的作业,比如下面就先创建了一个作业,jobs观察到一个作业。然后又创建了两个作业,此时有3个作业在运行,而且都是后台进程。
前面的数字是进程组的编号[1][2][3]。
输入指令fg+进程组编号,可以将后台进程变成前台进程,比如我们输入fg 1。此时第一个作业就变为了前端,进程Xshell窗口就阻塞住了,我们输入什么都不管用了。
使用Ctrl+Z可将该进程组暂停后,再次输入jobs可以看到进程组1后面的&没有了。这就表示它成为了后台进程,前面的stopped表示我们将其暂停了。
使用指令bg+进程组编号,可以将进程组设置入后台。此时进程组1后面的&又出现了,作业也运行了起来,也不再阻塞了,可以在窗口中继续输入指令了。
如果你查看sleep的进程,就能看到第二行后9个sleep进程的pid值都不同,这也证明它们都是独立的进程。
不过如果你仔细看第三行,还是会发现一些异同。
其中,第一行表示PPID也就是所有的9个sleep进程都是父进程bash创建的。
第二行表示PID,9个sleep进程各不一样。
第三行表示PGID,也就是进程组的ID,我用红绿蓝框框出的PGID相同,就证明它们属于同一个进程组,其中PID和PGID值相同的进程是这个进程组的组长。
第四行表示SID,是会话的ID,所有进程的SID都相同,同属于一个bash'会话。
3.将进程变为守护进程
系统调用setsid的作用就是将调用该函数的进程变成守护进程,也就是创建一个新的会话,这个会话中只有当前进程,但调用该系统调用的进程不能是进程组的组长。
如果调用成功,则返回新的会话SID,调用失败,则返回-1,并且设置错误码。
setsid()函数
pid_t setsid(void);
-
功能:将调用该函数的进程变成守护进程
-
参数:无
-
返回值:如果调用成功,则返回新的会话SID,调用失败,则返回-1,并且设置错误码。
我们再应以一个daemonself就可以让我们的tcp服务器变成守护进程,总共分为四步。
(1)调用进程忽略异常信号
守护进程隶属于另一个独立的会话,它的具体运行情况我们这个会话是无从得知的。此时它就i有可能受到异常信号的干扰而退出,而我们不会知道。为了避免这样的情况,就要忽略掉异常信号,尤其是关于管道的信号,毕竟网络操作本质就是文件操作,所以使用signal系统调用忽略掉SIGPIPE信号。
(2)自己不是组长
setsid系统调用要求调用的进程不能是进程组的组长。所以我们就可以同样采用之前多进程的策略。窗口进程创建服务器进程,服务器创建新进程,将后续代码交给它的子进程。此时原本bash的组长就退出了,新的子进程成为了孤儿进程被操作系统收养,操作系统的到进程组内它也就不是组长了。
然后,子进程就可以调用setsid变成守护进程。所以我们也可以认为,守护进程本质上就是一个孤儿进程。
(3)关闭或者重定向以前进程默认打开的文件
在Linux中存在一个黑洞文件/dev/null,向该文件中写入的内容会被全部丢弃。从该文件中读取内容时,虽然什么也读不到,但不会发生错误。
每个进程都会默认打开文件描述符为0,1,2的三个文件,但守护进程是脱离终端的,并没有显示器、键盘等设备文件,所以要对这三个文件重定向到这个黑洞文件/dev/null。如果无法重定向,关闭这三个文件也行。
(4)进程执行路径发生更改(可选)
每一进程都有一个cwd数据,用来记录当前进程的所属路径,所以默认情况下,进程文件所在的路径就是当前目录。
最终我们的server.cc代码如下:
#include"log.hpp"
#include"server.hpp"
#include<memory>
#include<unistd.h>
#include<fcntl.h>#define BLACKHOLE "/dev/null"
void daemonself(const char* cur_path = nullptr)
{//忽略管道信号signal(SIGPIPE, SIG_IGN);//自己不能成为组长if(fork()>0)exit(0);//父进程退出//子进程继续执行pid_t n = setsid();//变为守护进程assert(n != -1);//重定向三个默认打开的文件int fd = open(BLACKHOLE, O_RDWR);if(fd > 0){//打开成功,重定向dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);}else{//打开不成功,直接关上close(0);close(1);close(2);}//更改进程的执行路径(选做)if(cur_path)chdir(cur_path);
}static void Usage(string proc)
{printf("\nUsage:\n\t%s local_port\n\n",proc.c_str());
}
int main(int argc, char* argv[])
{if(argc != 2)//如果没输入端口号,argc保存的命令参数只有一个,进程出错{Usage(argv[0]);exit(USAGE_ERROR);}uint16_t port = atoi(argv[1]);unique_ptr<tcpserver> p(new tcpserver(port));p->initserver();daemonself();p->start();return 0;
}
再次编译打开后,可以看到只有initserver的代码打印在了屏幕上,而变为守护进程后start应当打印的字符都消失了。
当你打开客户端时,即使关闭了运行服务器的对话框,服务端也还在继续运行,我们可以通过客户端进行通信。
此时我们只能通过kill -9 pid的方式终止该进程。
4.关于daemon的说明
在unistd.h的头文件中,已经有一个系统调用daemon可以让一个进程变成守护进程。
int daemon(int nochdir, int noclose);
但是它不好用。在实际应用中,人们大多通过setsid自己实现daemon,就像我们上面做的一样。